mirror of
https://github.com/PHPOffice/PhpSpreadsheet.git
synced 2026-08-17 16:51:35 +00:00
Document Excel's Behavior When Sorting Different DataTypes
This commit is contained in:
@@ -43,4 +43,8 @@ Similar fraction formats have inconsistent results in Excel. For example, if a c
|
||||
|
||||
## COUNTIF and Text Cells
|
||||
|
||||
In Excel, COUNTIF appears to ignore text cells, behavior which doesn't seem to be documented anywhere. See [this issue](https://github.com/PHPOffice/PhpSpreadsheet/issues/3802), which remains open because, in the absence of usable documentation, we aren't sure how to handle things.
|
||||
In Excel, COUNTIF appears to ignore text cells, behavior which doesn't seem to be documented anywhere. See [this issue](https://github.com/PHPOffice/PhpSpreadsheet/issues/3802), which remains open because, in the absence of usable documentation, we aren't sure how to handle things.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -1207,6 +1207,8 @@ Code like the following can be used:
|
||||
// rowDividers: true,
|
||||
// rowHeaders: false,
|
||||
// columnHeaders: false,
|
||||
// Starting with release 5.4:
|
||||
// numbersRight: TextGridRightAlign::numeric,
|
||||
);
|
||||
$result = $textGrid->render();
|
||||
```
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Calculation\LookupRef;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Helper\Sample;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
|
||||
|
||||
class SortExcel
|
||||
{
|
||||
private static function cmp(mixed $a, mixed $b): int
|
||||
{
|
||||
// null sorts highest
|
||||
if (!is_scalar($a)) {
|
||||
return (is_scalar($b)) ? 1 : 0;
|
||||
}
|
||||
if (!is_scalar($b)) {
|
||||
return -1;
|
||||
}
|
||||
// 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) ? -1 : 1;
|
||||
}
|
||||
if ($numericA) {
|
||||
return -1;
|
||||
}
|
||||
if ($numericB) {
|
||||
return 1;
|
||||
}
|
||||
// bool sorts higher than string
|
||||
if (is_bool($a)) {
|
||||
if (!is_bool($b)) {
|
||||
return 1;
|
||||
}
|
||||
if ($a) {
|
||||
return $b ? 0 : 1;
|
||||
}
|
||||
|
||||
return $b ? -1 : 0;
|
||||
}
|
||||
if (is_bool($b)) {
|
||||
return -1;
|
||||
}
|
||||
// special handling for numeric strings starting with -
|
||||
$a = (string) preg_replace('/^-(\d)+$/', '$1', $a);
|
||||
$b = (string) preg_replace('/^-(\d)+$/', '$1', $b);
|
||||
|
||||
// strings sort case-insensitive
|
||||
return strcasecmp($a, $b);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort a one-dimensional array in the same order as Excel.
|
||||
*
|
||||
* @param array<int, null|bool|float|int|string> $array
|
||||
*/
|
||||
public static function sortArray(array &$array): void
|
||||
{
|
||||
usort($array, self::cmp(...));
|
||||
$j = count($array);
|
||||
while ($j > 0) {
|
||||
--$j;
|
||||
if ($array[$j] !== null) {
|
||||
break;
|
||||
}
|
||||
$array[$j] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
require __DIR__ . '/../Header.php';
|
||||
/** @var Sample $helper */
|
||||
$helper->log('Emulating how Excel sorts different DataTypes');
|
||||
|
||||
/** @param array<int, null|bool|float|int|string> $original */
|
||||
function displaySorted(array $original, Sample $helper): void
|
||||
{
|
||||
$sorted = $original;
|
||||
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);
|
||||
@@ -19,6 +19,12 @@ class Sort extends LookupRefValidations
|
||||
* The returned array is the same shape as the provided array argument.
|
||||
* Both $sortIndex and $sortOrder can be arrays, to provide multi-level sorting.
|
||||
*
|
||||
* NOTE: If $sortArray contains a mixture of data types
|
||||
* (string/int/bool), the results may be unexpected.
|
||||
* This is also true if the array consists of string
|
||||
* representations of numbers, especially if there are
|
||||
* both positive and negative numbers in the mix.
|
||||
*
|
||||
* @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
|
||||
@@ -77,6 +83,12 @@ class Sort extends LookupRefValidations
|
||||
* My interpretation of this is that by_array must be an
|
||||
* array which contains exactly one row.
|
||||
*
|
||||
* NOTE: If the "byArray" contains a mixture of data types
|
||||
* (string/int/bool), the results may be unexpected.
|
||||
* This is also true if the array consists of string
|
||||
* representations of numbers, especially if there are
|
||||
* both positive and negative numbers in the mix.
|
||||
*
|
||||
* @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
|
||||
|
||||
@@ -242,9 +242,12 @@ class Sample
|
||||
}
|
||||
|
||||
/** @param mixed[][] $matrix */
|
||||
public function displayGrid(array $matrix, ?bool $numbersRight = null): void
|
||||
public function displayGrid(array $matrix, null|bool|TextGridRightAlign $numbersRight = null): void
|
||||
{
|
||||
$renderer = new TextGrid($matrix, $this->isCli());
|
||||
if (is_bool($numbersRight)) {
|
||||
$numbersRight = $numbersRight ? TextGridRightAlign::numeric : TextGridRightAlign::none;
|
||||
}
|
||||
if ($numbersRight !== null) {
|
||||
$renderer->setNumbersRight($numbersRight);
|
||||
}
|
||||
|
||||
@@ -25,10 +25,10 @@ class TextGrid
|
||||
|
||||
protected bool $columnHeaders = true;
|
||||
|
||||
protected bool $numbersRight = false;
|
||||
protected TextGridRightAlign $numbersRight = TextGridRightAlign::none;
|
||||
|
||||
/** @param mixed[][] $matrix */
|
||||
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true, bool $numbersRight = false)
|
||||
public function __construct(array $matrix, bool $isCli = true, bool $rowDividers = false, bool $rowHeaders = true, bool $columnHeaders = true, TextGridRightAlign $numbersRight = TextGridRightAlign::none)
|
||||
{
|
||||
$this->rows = array_keys($matrix);
|
||||
$this->columns = array_keys($matrix[$this->rows[0]]);
|
||||
@@ -49,7 +49,7 @@ class TextGrid
|
||||
$this->numbersRight = $numbersRight;
|
||||
}
|
||||
|
||||
public function setNumbersRight(bool $numbersRight): void
|
||||
public function setNumbersRight(TextGridRightAlign $numbersRight): void
|
||||
{
|
||||
$this->numbersRight = $numbersRight;
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class TextGrid
|
||||
$valueForLength = $this->getString($cell);
|
||||
$displayCell = $this->isCli ? $valueForLength : htmlentities($valueForLength);
|
||||
$this->gridDisplay .= '| ';
|
||||
if ($this->rightAlign($displayCell)) {
|
||||
if ($this->rightAlign($displayCell, $cell)) {
|
||||
$this->gridDisplay .= str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength)) . $displayCell . ' ';
|
||||
} else {
|
||||
$this->gridDisplay .= $displayCell . str_repeat(' ', $columnWidths[$column] - $this->strlen($valueForLength) + 1);
|
||||
@@ -108,9 +108,9 @@ class TextGrid
|
||||
}
|
||||
}
|
||||
|
||||
protected function rightAlign(string $displayCell): bool
|
||||
protected function rightAlign(string $displayCell, mixed $cell = null): bool
|
||||
{
|
||||
return $this->numbersRight && is_numeric($displayCell);
|
||||
return ($this->numbersRight === TextGridRightAlign::numeric && is_numeric($displayCell)) || ($this->numbersRight === TextGridRightAlign::floatOrInt && (is_int($cell) || is_float($cell)));
|
||||
}
|
||||
|
||||
/** @param int[] $columnWidths */
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace PhpOffice\PhpSpreadsheet\Helper;
|
||||
|
||||
enum TextGridRightAlign
|
||||
{
|
||||
case none;
|
||||
case numeric;
|
||||
case floatOrInt;
|
||||
}
|
||||
@@ -5,12 +5,13 @@ declare(strict_types=1);
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Helper;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGrid;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
|
||||
|
||||
class TextGridExtended extends TextGrid
|
||||
{
|
||||
protected function rightAlign(string $displayCell): bool
|
||||
protected function rightAlign(string $displayCell, mixed $cell = null): bool
|
||||
{
|
||||
// regexp is imperfect, but good enough for test purposes
|
||||
return $this->numbersRight && preg_match('/^[-+$,.0-9]+$/', $displayCell);
|
||||
return is_int($cell) || is_float($cell) || ($this->numbersRight === TextGridRightAlign::numeric && preg_match('/^[-+$,.0-9]+$/', $displayCell));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace PhpOffice\PhpSpreadsheetTests\Helper;
|
||||
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGrid;
|
||||
use PhpOffice\PhpSpreadsheet\Helper\TextGridRightAlign;
|
||||
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -215,7 +216,7 @@ class TextGridTest extends TestCase
|
||||
rowDividers: false,
|
||||
rowHeaders: false,
|
||||
columnHeaders: false,
|
||||
numbersRight: true,
|
||||
numbersRight: TextGridRightAlign::numeric,
|
||||
);
|
||||
$expected = [
|
||||
'+-----+-------+--------------+',
|
||||
@@ -251,7 +252,7 @@ class TextGridTest extends TestCase
|
||||
rowHeaders: false,
|
||||
columnHeaders: false,
|
||||
);
|
||||
$textGrid->setNumbersRight(true);
|
||||
$textGrid->setNumbersRight(TextGridRightAlign::numeric);
|
||||
$expected = [
|
||||
'+-----+-------+--------------+',
|
||||
'| 0 | 1.00 | $1,234.56 |',
|
||||
|
||||
Reference in New Issue
Block a user