From f0343ae2bf2a8c93efe5cc463cafd4e3a7df24a8 Mon Sep 17 00:00:00 2001 From: Pol Dellaiera Date: Fri, 19 Jan 2024 10:47:00 +0100 Subject: [PATCH] feat: implements stable sort (#52) --- LICENSE | 2 +- README.md | 28 +++++++++++++++ src/SortIterableAggregate.php | 43 +++++++++++++++++++++--- tests/unit/SortIterableAggregateTest.php | 23 +++++++++++++ 4 files changed, 90 insertions(+), 6 deletions(-) diff --git a/LICENSE b/LICENSE index 7a68458..27759d8 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2021-2023 Pol Dellaiera +Copyright (c) 2021-2024 Pol Dellaiera Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/README.md b/README.md index 7ccec9c..4470cbd 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ The missing PHP iterators. - `ReductionIterableAggregate` - `ResourceIteratorAggregate` - `SimpleCachingIteratorAggregate` +- `SortIterableAggregate` - `StringIteratorAggregate` - `TypedIterableAggregate` - `UniqueIterableAggregate` @@ -321,6 +322,33 @@ $iterator = (new ReductionIterableAggregate( foreach ($iterator as $reduction) {} // [0, 1, 3, 6, 10, 15, 21, 28, 36, 45, 55] ``` +### SortIterableAggregate + +Implements a +[stable](https://en.m.wikipedia.org/wiki/Sorting_algorithm#Stability) sort +iterable aggregate + +This means that if two elements have the same key, the one that appeared earlier +in the input will also appear earlier in the sorted output. + +```php +$valueObjectFactory = static fn (int $id, int $weight) => new class($id, $weight) +{ + public function __construct(public readonly int $id, public readonly int $weight) {} +}; + +$input = [ + $valueObjectFactory(id: 1, weight: 1), + $valueObjectFactory(id: 2, weight: 1), + $valueObjectFactory(id: 3, weight: 1), +]; + +$sort = new SortIterableAggregate( + $input, + static fn (object $a, object $b): int => $a->weight <=> $b->weight +); +``` + ## Code quality, tests, benchmarks Every time changes are introduced into the library, [Github][2] runs the tests. diff --git a/src/SortIterableAggregate.php b/src/SortIterableAggregate.php index 4242f69..aab2c79 100644 --- a/src/SortIterableAggregate.php +++ b/src/SortIterableAggregate.php @@ -7,26 +7,59 @@ use Closure; use Generator; use IteratorAggregate; +use SplHeap; /** * @template TKey * @template T * - * @implements IteratorAggregate + * @implements IteratorAggregate */ final class SortIterableAggregate implements IteratorAggregate { /** * @param iterable $iterable - * @param (Closure(T, T): int) $callback + * @param Closure(T, T, TKey, TKey): int $callback */ - public function __construct(private iterable $iterable, private Closure $callback) {} + public function __construct(private readonly iterable $iterable, private readonly Closure $callback) {} /** - * @return Generator + * @return Generator */ public function getIterator(): Generator { - yield from new SortIterator($this->iterable, $this->callback); + $iterator = new /** + * @template T + * @template TKey + * + * @param iterable $iterable + * @param Closure(T, T, TKey, TKey): int $callback + * + * @extends SplHeap + */ class($this->iterable, $this->callback) extends SplHeap { + /** + * @param iterable $iterable + * @param Closure(T, T, TKey, TKey): int $callback + */ + public function __construct(iterable $iterable, private Closure $callback) + { + foreach (new PackIterableAggregate($iterable) as $key => $value) { + $this->insert([$key, $value]); + } + } + + /** + * @param array{0: int, 1:array{0:TKey, 1:T}}|mixed $value1 + * @param array{0: int, 1:array{0:TKey, 1:T}}|mixed $value2 + */ + protected function compare($value1, $value2): int + { + return (0 === $return = ($this->callback)($value1[1][1], $value2[1][1], $value1[1][0], $value2[1][0])) ? $value2[0] <=> $value1[0] : $return; + } + }; + + foreach (new UnpackIterableAggregate($iterator) as $value) { + yield $value[0] => $value[1]; + } } } diff --git a/tests/unit/SortIterableAggregateTest.php b/tests/unit/SortIterableAggregateTest.php index 20e5074..e1f37a4 100644 --- a/tests/unit/SortIterableAggregateTest.php +++ b/tests/unit/SortIterableAggregateTest.php @@ -9,6 +9,7 @@ namespace tests\loophp\iterators; +use loophp\iterators\MapIterableAggregate; use loophp\iterators\SortIterableAggregate; use PHPUnit\Framework\TestCase; @@ -48,4 +49,26 @@ public function testSimpleSort(): void self::assertSame($expected, range('a', 'c')); } + + public function testStableSort(): void + { + $valueObjectFactory = static fn (int $id, int $weight) => new class($id, $weight) { + public function __construct( + public readonly int $id, + public readonly int $weight, + ) {} + }; + + $input = [ + $valueObjectFactory(id: 1, weight: 1), + $valueObjectFactory(id: 2, weight: 1), + $valueObjectFactory(id: 3, weight: 1), + ]; + + $sort = new SortIterableAggregate($input, static fn (object $a, object $b): int => $a->weight <=> $b->weight); + + $expected = [1, 2, 3]; + + self::assertSame($expected, iterator_to_array(new MapIterableAggregate($sort, static fn (object $value): int => $value->id))); + } }