sort natively in sortBy and sort
This commit is contained in:
parent
dc1b684978
commit
a4a51e32a1
@ -268,6 +268,9 @@ A few consequences worth knowing:
|
||||
- `merge()` merges arrays and `Ds\Map` by key (integer keys of arrays are kept, unlike `array_merge()`); for other
|
||||
inputs it appends.
|
||||
- `sort()`/`sortBy()` are stable and never modify their input, unlike `Ds\Seq::sort()`.
|
||||
- `sort()` without a comparator and `sortBy()` sort natively, comparing like `<=>`. The one difference is enums,
|
||||
which `<=>` can't order: they are grouped by case and placed after the other values (before them in descending
|
||||
order).
|
||||
|
||||
### Finding things: `first`, `find` and `required`
|
||||
|
||||
|
||||
@ -147,6 +147,28 @@ function entries(iterable $iterable): array
|
||||
return $entries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a `Ds\Map` from entries given by position, in the order of the keys of $order (positions).
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @param list<mixed> $keys
|
||||
* @param array<int, mixed> $values
|
||||
* @param array<int, mixed> $order
|
||||
*
|
||||
* @return \Ds\Map<mixed, mixed>
|
||||
*/
|
||||
function mapInOrder(array $keys, array $values, array $order): \Ds\Map
|
||||
{
|
||||
$result = new \Ds\Map();
|
||||
$result->allocate(count($order));
|
||||
foreach ($order as $position => $_) {
|
||||
$result->put($keys[$position], $values[$position]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields [key, value] pairs back as key => value.
|
||||
*
|
||||
|
||||
@ -27,6 +27,7 @@ use function Kadet\Functional\Collections\Internal\iterated;
|
||||
use function Kadet\Functional\Collections\Internal\keyedLike;
|
||||
use function Kadet\Functional\Collections\Internal\seen;
|
||||
use function Kadet\Functional\Collections\Internal\listLike;
|
||||
use function Kadet\Functional\Collections\Internal\mapInOrder;
|
||||
use function Kadet\Functional\Collections\Internal\seqOf;
|
||||
use function Kadet\Functional\Collections\Internal\unpairs;
|
||||
use function Kadet\Functional\Collections\Internal\valuesOf;
|
||||
@ -314,14 +315,51 @@ function uniqueBy(callable $selector): \Closure
|
||||
*/
|
||||
function sort(?callable $comparator = null): \Closure
|
||||
{
|
||||
if ($comparator === null) {
|
||||
// The native SORT_REGULAR sorts compare like `<=>` and are stable.
|
||||
return static function (iterable $input): iterable {
|
||||
if (is_array($input)) {
|
||||
asort($input);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
if ($input instanceof \Ds\Map) {
|
||||
$keys = $input->keys()->toArray();
|
||||
$values = $input->values()->toArray();
|
||||
asort($values);
|
||||
|
||||
return mapInOrder($keys, $values, $values);
|
||||
}
|
||||
|
||||
$values = valuesOf($input);
|
||||
\sort($values);
|
||||
|
||||
return listLike($input, $values);
|
||||
};
|
||||
}
|
||||
|
||||
// Comparators always take two values, so they are compiled without the ($value, $key) adaptation.
|
||||
$comparator = $comparator === null ? static fn ($a, $b) => $a <=> $b : compile($comparator);
|
||||
$comparator = compile($comparator);
|
||||
|
||||
return static function (iterable $input) use ($comparator): iterable {
|
||||
if (is_array($input)) {
|
||||
uasort($input, $comparator);
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
if ($input instanceof \Ds\Map) {
|
||||
$entries = entries($input);
|
||||
usort($entries, static fn ($a, $b) => $comparator($a[1], $b[1]));
|
||||
|
||||
return keyedLike($input, unpairs($entries));
|
||||
}
|
||||
|
||||
$values = valuesOf($input);
|
||||
usort($values, $comparator);
|
||||
|
||||
return listLike($input, $values);
|
||||
};
|
||||
}
|
||||
|
||||
@ -348,16 +386,47 @@ function sortBy(callable $selector, bool $descending = false): \Closure
|
||||
{
|
||||
$selector = callback($selector);
|
||||
|
||||
// The selected values are sorted natively: SORT_REGULAR compares like `<=>`, and PHP's sorts are stable, also
|
||||
// arsort() for ties.
|
||||
return static function (iterable $input) use ($selector, $descending): iterable {
|
||||
$entries = [];
|
||||
if (is_array($input)) {
|
||||
$order = [];
|
||||
foreach ($input as $key => $value) {
|
||||
$entries[] = [$key, $value, $selector($value, $key)];
|
||||
$order[$key] = $selector($value, $key);
|
||||
}
|
||||
|
||||
$direction = $descending ? -1 : 1;
|
||||
usort($entries, static fn ($a, $b) => $direction * ($a[2] <=> $b[2]));
|
||||
$descending ? arsort($order) : asort($order);
|
||||
|
||||
return keyedLike($input, unpairs($entries));
|
||||
$result = [];
|
||||
foreach ($order as $key => $_) {
|
||||
$result[$key] = $input[$key];
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
// Keys of `Ds\Map` can be objects, so the entries are sorted by position.
|
||||
$keys = [];
|
||||
$values = [];
|
||||
$order = [];
|
||||
foreach (iterated($input) as $key => $value) {
|
||||
$keys[] = $key;
|
||||
$values[] = $value;
|
||||
$order[] = $selector($value, $key);
|
||||
}
|
||||
|
||||
$descending ? arsort($order) : asort($order);
|
||||
|
||||
if ($input instanceof \Ds\Map) {
|
||||
return mapInOrder($keys, $values, $order);
|
||||
}
|
||||
|
||||
$result = [];
|
||||
foreach ($order as $position => $_) {
|
||||
$result[] = $values[$position];
|
||||
}
|
||||
|
||||
return listLike($input, $result);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -248,6 +248,81 @@ class TransformsTest extends \PHPUnit\Framework\TestCase
|
||||
$this->assertSame(['b' => 1, 'a' => 2], c\sortBy(fn ($v, $k) => $k, true)(['a' => 2, 'b' => 1]));
|
||||
}
|
||||
|
||||
public function testSortByIsStableForTies()
|
||||
{
|
||||
$items = [['k' => 1, 'n' => 'a'], ['k' => 0, 'n' => 'b'], ['k' => 1, 'n' => 'c'], ['k' => 0, 'n' => 'd'], ['k' => 1, 'n' => 'e']];
|
||||
|
||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'])($items), 'n'));
|
||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'])(new Seq($items))->toArray(), 'n'));
|
||||
}
|
||||
|
||||
public function testSortByDescendingIsStableForTies()
|
||||
{
|
||||
$items = [['k' => 0, 'n' => 'a'], ['k' => 1, 'n' => 'b'], ['k' => 0, 'n' => 'c'], ['k' => 1, 'n' => 'd'], ['k' => 0, 'n' => 'e']];
|
||||
|
||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'], true)($items), 'n'));
|
||||
$this->assertSame(['b', 'd', 'a', 'c', 'e'], array_column(c\sortBy(fn ($o) => $o['k'], true)(new Seq($items))->toArray(), 'n'));
|
||||
}
|
||||
|
||||
public function testSortByWithDateTimeKeys()
|
||||
{
|
||||
$at = fn (string $time) => new \DateTimeImmutable("2026-01-01 $time");
|
||||
$items = ['c' => $at('10:00'), 'a' => $at('08:00'), 'd' => $at('10:00:01'), 'b' => $at('09:30')];
|
||||
|
||||
$this->assertSame(['a', 'b', 'c', 'd'], array_keys(c\sortBy(fn ($v) => $v)($items)));
|
||||
$this->assertSame(['d', 'c', 'b', 'a'], array_keys(c\sortBy(fn ($v) => $v, true)($items)));
|
||||
}
|
||||
|
||||
public function testSortByMixedNumericKeys()
|
||||
{
|
||||
$values = [3, 1.5, -2, 2.0, 0, 1, -0.5];
|
||||
|
||||
$this->assertSame([-2, -0.5, 0, 1, 1.5, 2.0, 3], c\sortBy(fn ($v) => $v)(new Seq($values))->toArray());
|
||||
$this->assertSame([3, 2.0, 1.5, 1, 0, -0.5, -2], array_values(c\sortBy(fn ($v) => $v, true)($values)));
|
||||
}
|
||||
|
||||
public function testSortAndSortByCompareLikeSpaceship()
|
||||
{
|
||||
$keys = [10, '9', 'abc', null, 2.5, '10', true, 'b', '', 0, [1, 2], [1], '1e1', ' 9', false];
|
||||
|
||||
$ascending = $keys;
|
||||
usort($ascending, fn ($a, $b) => $a <=> $b);
|
||||
$descending = $keys;
|
||||
usort($descending, fn ($a, $b) => $b <=> $a);
|
||||
|
||||
$this->assertSame($ascending, array_values(c\sortBy(fn ($v) => $v)($keys)));
|
||||
$this->assertSame($descending, array_values(c\sortBy(fn ($v) => $v, true)($keys)));
|
||||
$this->assertSame($ascending, array_values(c\sort()($keys)));
|
||||
}
|
||||
|
||||
public function testSortByKeepsObjectKeysOfMap()
|
||||
{
|
||||
$a = new \stdClass();
|
||||
$b = new \stdClass();
|
||||
$map = new Map();
|
||||
$map->put($a, 2);
|
||||
$map->put($b, 1);
|
||||
|
||||
$sorted = c\sortBy(fn ($v) => $v)($map);
|
||||
|
||||
$this->assertInstanceOf(Map::class, $sorted);
|
||||
$this->assertSame([$b, $a], $sorted->keys()->toArray());
|
||||
$this->assertSame([1, 2], $sorted->values()->toArray());
|
||||
}
|
||||
|
||||
public function testSortByDoesNotMutateInput()
|
||||
{
|
||||
$array = ['x' => 3, 'y' => 1, 'z' => 2];
|
||||
$seq = new Seq([3, 1, 2]);
|
||||
|
||||
c\sortBy(fn ($v) => $v)($array);
|
||||
c\sortBy(fn ($v) => $v, true)($seq);
|
||||
c\sort()($array);
|
||||
|
||||
$this->assertSame(['x' => 3, 'y' => 1, 'z' => 2], $array);
|
||||
$this->assertSame([3, 1, 2], $seq->toArray());
|
||||
}
|
||||
|
||||
public function testSortDoesNotMutateInput()
|
||||
{
|
||||
$seq = new Seq([3, 1, 2]);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user