Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sort: improve sort operation #334

Merged
merged 1 commit into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
],
"require": {
"php": ">= 8.1",
"loophp/iterators": "^3"
"loophp/iterators": "^3.1"
},
"require-dev": {
"ext-pcov": "*",
Expand Down
13 changes: 8 additions & 5 deletions docs/pages/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2135,18 +2135,21 @@ Signature: ``Collection::slice(int $offset, ?int $length = -1): Collection;``
sort
~~~~

Sort a collection using a callback. If no callback is provided, it will sort using natural order.
Sort a collection using a callback. If no callback is provided, it will sort
using natural order, ascending.

By default, it will sort by values and using a callback. If you want to sort by keys, you can pass a parameter to change
the behaviour or use twice the flip operation. See the example below.
By default, it will sort by values and using the default callback. If you want
to sort by keys, you can pass a parameter to change the behaviour.

Since version 7.4, sorting is `stable` by default. Stable sort algorithms sort equal
elements in the same order that they appear in the input.
Since version 7.4, sorting is `stable` by default. Stable sort algorithms sort
equal elements in the same order that they appear in the input.

Interface: `Sortable`_

Signature: ``Collection::sort(int $type = Sortable::BY_VALUES, ?callable $callback = null): Collection;``

Callback signature: ``Closure(mixed $right, mixed $left, mixed $rightKey, mixed $leftKey): int``

.. literalinclude:: code/operations/sort.php
:language: php

Expand Down
13 changes: 10 additions & 3 deletions docs/pages/code/operations/sort.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,21 @@
$collection = Collection::fromIterable(['z', 'y', 'x'])
->sort(
Sortable::BY_VALUES,
static fn ($left, $right): int => $left <=> $right
static fn (string $left, string $right): int => $left <=> $right
); // [0 => 'z', 1 => 'y', 2 => 'x']

// Example 3 -> Regular values sorting with a custom callback, inverted
$collection = Collection::fromIterable(['z', 'y', 'x'])
->sort(
Sortable::BY_VALUES,
static fn (string $left, string $right): int => $right <=> $left
); // [2 => 'x', 1 => 'y', 0 => 'z']

// Example 3 -> Regular keys sorting (no callback is needed here)
// Example 4 -> Regular keys sorting (no callback is needed here)
$collection = Collection::fromIterable([3 => 'z', 2 => 'y', 1 => 'x'])
->sort(Sortable::BY_KEYS); // [1 => 'x', 2 => 'y', 3 => 'z']

// Example 4 -> Regular keys sorting using the flip() operation twice
// Example 5 -> Regular keys sorting using the flip() operation twice
$collection = Collection::fromIterable([3 => 'z', 2 => 'y', 1 => 'x'])
->flip() // Exchange values and keys
->sort() // Sort the values (which are now the keys)
Expand Down
2 changes: 1 addition & 1 deletion src/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@

public function combinate(?int $length = null): CollectionInterface
{
return new self((new Operation\Combinate())()($length), [$this]);

Check failure on line 117 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

MixedArgument

src/Collection.php:117:25: MixedArgument: Argument 1 of loophp\collection\Collection::__construct cannot be mixed, expecting callable(mixed...):iterable<mixed, mixed> (see https://psalm.dev/030)
}

public function combine(mixed ...$keys): CollectionInterface
Expand Down Expand Up @@ -146,7 +146,7 @@
{
return new self(
(new Operation\Apply())()(
static function () use (&$counter): void {

Check failure on line 149 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

InvalidArgument

src/Collection.php:149:17: InvalidArgument: Argument 1 expects callable(mixed, mixed, iterable<mixed, mixed>):bool, but impure-Closure():void provided (see https://psalm.dev/004)
++$counter;
}
),
Expand Down Expand Up @@ -267,7 +267,7 @@

public function every(callable ...$callbacks): bool
{
return (new Operation\Every())()(static fn (int $index, mixed $value, mixed $key, iterable $iterable): bool => CallbacksArrayReducer::or()($callbacks)($value, $key, $iterable))($this)

Check failure on line 270 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

InvalidArgument

src/Collection.php:270:148: InvalidArgument: Argument 1 expects array<array-key, callable(mixed...):bool>, but array<array-key, callable(T:loophp\collection\Collection as mixed, TKey:loophp\collection\Collection as mixed, iterable<TKey:loophp\collection\Collection as mixed, T:loophp\collection\Collection as mixed>):bool> provided (see https://psalm.dev/004)
->current();
}

Expand All @@ -288,7 +288,7 @@

public function find(mixed $default = null, callable ...$callbacks)
{
return (new Operation\Find())()($default)(...$callbacks)($this)->current();

Check failure on line 291 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

PossiblyNullArgument

src/Collection.php:291:41: PossiblyNullArgument: Argument 1 cannot be null, possibly null value provided (see https://psalm.dev/078)
}

public function first(mixed $default = null)
Expand Down Expand Up @@ -413,7 +413,7 @@

public function get(mixed $key, mixed $default = null)
{
return (new self((new Operation\Get())()($key)($default), [$this]))->current(0, $default);

Check failure on line 416 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

PossiblyNullArgument

src/Collection.php:416:56: PossiblyNullArgument: Argument 1 cannot be null, possibly null value provided (see https://psalm.dev/078)
}

/**
Expand Down Expand Up @@ -595,9 +595,9 @@
return new self((new Operation\Pair())(), [$this]);
}

public function partition(callable ...$callbacks): CollectionInterface

Check failure on line 598 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

InvalidReturnType

src/Collection.php:598:56: InvalidReturnType: The declared return type 'loophp\collection\Contract\Collection<int, loophp\collection\Collection<TKey:loophp\collection\Collection as mixed, T:loophp\collection\Collection as mixed>>' for loophp\collection\Collection::partition is incorrect, got 'loophp\collection\Contract\Collection<int, loophp\collection\Collection<int, iterable<mixed, mixed>>>&loophp\collection\Contract\Collection' (see https://psalm.dev/011)
{
return (new self((new Operation\Partition())()(...$callbacks), [$this]))

Check failure on line 600 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

InvalidReturnStatement

src/Collection.php:600:16: InvalidReturnStatement: The inferred type 'loophp\collection\Contract\Collection<int, loophp\collection\Collection<int, iterable<mixed, mixed>>>&loophp\collection\Contract\Collection' does not match the declared return type 'loophp\collection\Contract\Collection<int, loophp\collection\Collection<TKey:loophp\collection\Collection as mixed, T:loophp\collection\Collection as mixed>>' for loophp\collection\Collection::partition (see https://psalm.dev/128)
->map(
/**
* @param iterable<TKey, T> $iterable
Expand Down Expand Up @@ -630,7 +630,7 @@

public function product(iterable ...$iterables): CollectionInterface
{
return new self((new Operation\Product())()(...$iterables), [$this]);

Check failure on line 633 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

MixedArgument

src/Collection.php:633:25: MixedArgument: Argument 1 of loophp\collection\Collection::__construct cannot be mixed, expecting callable(mixed...):iterable<mixed, mixed> (see https://psalm.dev/030)
}

public function random(int $size = 1, ?int $seed = null): CollectionInterface
Expand Down Expand Up @@ -717,7 +717,7 @@

public function scanRight1(callable $callback): CollectionInterface
{
return new self((new Operation\ScanRight1())()($callback), [$this]);

Check failure on line 720 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

InvalidArgument

src/Collection.php:720:56: InvalidArgument: Argument 1 expects callable(mixed, mixed, mixed, iterable<mixed, mixed>):mixed, but callable((T:loophp\collection\Collection as mixed)|(V:fn-loophp\collection\contract\operation\scanright1able::scanright1 as mixed), T:loophp\collection\Collection as mixed, TKey:loophp\collection\Collection as mixed, Iterator<TKey:loophp\collection\Collection as mixed, T:loophp\collection\Collection as mixed>):((T:loophp\collection\Collection as mixed)|(V:fn-loophp\collection\contract\operation\scanright1able::scanright1 as mixed)) provided (see https://psalm.dev/004)
}

public function shuffle(?int $seed = null): CollectionInterface
Expand All @@ -735,12 +735,12 @@
return new self((new Operation\Slice())()($offset)($length), [$this]);
}

public function sort(int $type = OperationInterface\Sortable::BY_VALUES, ?callable $callback = null): CollectionInterface
public function sort(int $type = OperationInterface\Sortable::BY_VALUES, null|callable|Closure $callback = null): CollectionInterface
{
return new self((new Operation\Sort())()($type)($callback), [$this]);
}

public function span(callable ...$callbacks): CollectionInterface

Check failure on line 743 in src/Collection.php

View workflow job for this annotation

GitHub Actions / Static Analysis (ubuntu-latest, 8.1)

InvalidReturnType

src/Collection.php:743:51: InvalidReturnType: The declared return type 'loophp\collection\Contract\Collection<int, loophp\collection\Collection<TKey:loophp\collection\Collection as mixed, T:loophp\collection\Collection as mixed>>' for loophp\collection\Collection::span is incorrect, got 'loophp\collection\Contract\Collection<int, loophp\collection\Collection<int, iterable<mixed, mixed>>>&loophp\collection\Contract\Collection' (see https://psalm.dev/011)
{
return (new self((new Operation\Span())()(...$callbacks), [$this]))
->map(
Expand Down
2 changes: 1 addition & 1 deletion src/CollectionDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@ public function slice(int $offset, int $length = -1): static
return new static($this->innerCollection->slice($offset, $length));
}

public function sort(int $type = Operation\Sortable::BY_VALUES, ?callable $callback = null): static
public function sort(int $type = Operation\Sortable::BY_VALUES, null|callable|Closure $callback = null): static
{
return new static($this->innerCollection->sort($type, $callback));
}
Expand Down
5 changes: 4 additions & 1 deletion src/Contract/Operation/Sortable.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace loophp\collection\Contract\Operation;

use Closure;
use loophp\collection\Contract\Collection;

/**
Expand All @@ -23,7 +24,9 @@ interface Sortable
*
* @see https://loophp-collection.readthedocs.io/en/stable/pages/api.html#sort
*
* @param null|callable|Closure(T, T, TKey, TKey): int $callback
*
* @return Collection<TKey, T>
*/
public function sort(int $type = Sortable::BY_VALUES, ?callable $callback = null): Collection;
public function sort(int $type = Sortable::BY_VALUES, null|callable|Closure $callback = null): Collection;
}
2 changes: 1 addition & 1 deletion src/Operation/Matching.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ static function (Criteria $criteria): Closure {
$next = null;

foreach (array_reverse($orderings) as $field => $ordering) {
$next = ClosureExpressionVisitor::sortByField($field, Criteria::DESC === $ordering ? -1 : 1, $next);
$next = ClosureExpressionVisitor::sortByField($field, Criteria::ASC === $ordering ? -1 : 1, $next);
}

$pipes[] = (new Sort())()(Sortable::BY_VALUES)($next);
Expand Down
1 change: 0 additions & 1 deletion src/Operation/Scale.php
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@ static function (float|int $v) use ($lowerBound, $upperBound, $wantedLowerBound,

$filter = (new Filter())()(
static fn (float|int $item): bool => $item > $lowerBound,

static fn (float|int $item): bool => $item <= $upperBound
);

Expand Down
116 changes: 56 additions & 60 deletions src/Operation/Sort.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
use Exception;
use Generator;
use loophp\collection\Contract\Operation;
use loophp\iterators\SortIterator;
use loophp\iterators\SortIterableAggregate;

/**
* @immutable
Expand All @@ -19,76 +19,72 @@
final class Sort extends AbstractOperation
{
/**
* @return Closure(int): Closure(null|(callable(T|TKey, T|TKey): int)): Closure(iterable<TKey, T>): Generator<TKey, T>
* @return Closure(int): Closure(null|(Closure(T, T, TKey, TKey): int)): Closure(iterable<TKey, T>): Generator<TKey, T>
*/
public function __invoke(): Closure
{
return
/**
* @return Closure(null|(callable(T|TKey, T|TKey): int)): Closure(iterable<TKey, T>): Generator<TKey, T>
* @return Closure(null|Closure(T, T, TKey, TKey): int): Closure(iterable<TKey, T>): Generator<TKey, T>
*/
static fn (int $type = Operation\Sortable::BY_VALUES): Closure =>
/**
* @param null|(callable(T|TKey, T|TKey): int) $callback
*
* @return Closure(iterable<TKey, T>): Generator<TKey, T>
*/
static function (?callable $callback = null) use ($type): Closure {
$callback ??=
/**
* @param T|TKey $left
* @param T|TKey $right
*/
static fn (mixed $left, mixed $right): int => $left <=> $right;
/**
* @param null|(Closure(T, T, TKey, TKey): int)|(callable(T, T, TKey, TKey): int) $callback
*
* @return Closure(iterable<TKey, T>): Generator<TKey, T>
*/
static function (null|callable|Closure $callback = null) use ($type): Closure {
if (Operation\Sortable::BY_VALUES !== $type && Operation\Sortable::BY_KEYS !== $type) {
throw new Exception('Invalid sort type.');
}

$callback ??=
/**
* @param T $left
* @param T $right
* @param TKey $leftKey
* @param TKey $rightKey
*/
static fn (mixed $left, mixed $right, mixed $leftKey, mixed $rightKey): int => $right <=> $left;

if (!($callback instanceof Closure)) {
trigger_deprecation(
'loophp/collection',
'7.4',
'Passing a callable as argument is deprecated and will be removed in 8.0. Use a closure instead.',
self::class
);

return
/**
* @param iterable<TKey, T> $iterable
*
* @return Generator<TKey, T>
*/
static function (iterable $iterable) use ($type, $callback): Generator {
if (Operation\Sortable::BY_VALUES !== $type && Operation\Sortable::BY_KEYS !== $type) {
throw new Exception('Invalid sort type.');
}
$callback = Closure::fromCallable($callback);
}

$operations = Operation\Sortable::BY_VALUES === $type ?
[
'before' => [(new Pack())()],
'after' => [(new Unpack())()],
] :
[
'before' => [(new Flip())(), (new Pack())()],
'after' => [(new Unpack())(), (new Flip())()],
];
$operations = Operation\Sortable::BY_VALUES === $type ?
[
'before' => [],
'after' => [],
] :
[
'before' => [(new Flip())()],
'after' => [(new Flip())()],
];

$sortCallback =
/**
* @param callable(T|TKey, T|TKey): int $callback
*
* @return Closure(array{0:TKey|T, 1:T|TKey}, array{0:TKey|T, 1:T|TKey}): int
*/
static fn (callable $callback): Closure =>
/**
* @param array{0:TKey|T, 1:T|TKey} $left
* @param array{0:TKey|T, 1:T|TKey} $right
*/
static fn (array $left, array $right): int => (0 === $return = $callback($right[1], $left[1])) ? ($right[0] <=> $left[0]) : $return;
$sortedIterator =
/**
* @param iterable<TKey, T> $iterable
*
* @return SortIterableAggregate<TKey, T>
*/
static fn (iterable $iterable): SortIterableAggregate => new SortIterableAggregate($iterable, $callback);

$sortedIterator =
/**
* @param iterable<TKey, T> $iterable
*
* @return SortIterator<TKey, T>
*/
static fn (iterable $iterable): SortIterator => new SortIterator($iterable, $sortCallback($callback));
/** @var Closure(iterable<TKey, T>): Generator<TKey, T> $sort */
$sort = (new Pipe())()(
...$operations['before'],
...[$sortedIterator],
...$operations['after']
);

yield from (new Pipe())()(
...$operations['before'],
...[$sortedIterator],
...$operations['after']
)($iterable);
};
};
// Point free style.
return $sort;
};
}
}
2 changes: 1 addition & 1 deletion tests/unit/Traits/GenericCollectionProviders.php
Original file line number Diff line number Diff line change
Expand Up @@ -4068,7 +4068,7 @@ public static function sortOperationProvider()
$operation,
[
Operation\Sortable::BY_VALUES,
static fn ($left, $right): int => $right <=> $left,
static fn (string $left, string $right): int => $left <=> $right,
],
$input,
array_combine(range('A', 'E'), range('E', 'A')),
Expand Down