From fa0e4ad5cd42192c70edf96dda54f5177ca44024 Mon Sep 17 00:00:00 2001 From: Bl00D4NGEL Date: Fri, 5 Jan 2024 16:52:15 +0100 Subject: [PATCH] feat: add first, firstOr, last, lastOr functions to collection --- src/Domain/Collection.php | 108 ++++++++++++++++++++++++++++++-- tests/Domain/CollectionTest.php | 95 ++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+), 4 deletions(-) diff --git a/src/Domain/Collection.php b/src/Domain/Collection.php index 5b8c42c..1ed02a9 100644 --- a/src/Domain/Collection.php +++ b/src/Domain/Collection.php @@ -31,7 +31,7 @@ final public function __construct( /** * Creates a collection from a given iterable of items. - * This function is useful when trying to create a collection from a generator or an iterator + * This function is useful when trying to create a collection from a generator or an iterator. * * @param iterable $items * @param class-string|null $itemType @@ -54,7 +54,7 @@ public static function fromIterable(iterable $items, ?string $itemType = null): /** * Returns true if every value in the collection passes the callback truthy test. Opposite of self::none(). * Callback arguments will be element, index, collection. - * Function short-circuits on first falsy return value + * Function short-circuits on first falsy return value. * * @param ?callable(T, int, static): bool $callback * @return bool @@ -77,7 +77,7 @@ public function every(callable $callback = null): bool /** * Returns true if every value in the collection passes the callback falsy test. Opposite of self::every(). * Callback arguments will be element, index, collection. - * Function short-circuits on first truthy return value + * Function short-circuits on first truthy return value. * * @param ?callable(T, int, static): bool $callback * @return bool @@ -100,7 +100,7 @@ public function none(callable $callback = null): bool /** * Returns true if at least one value in the collection passes the callback truthy test. * Callback arguments will be element, index, collection. - * Function short-circuits on first truthy return value + * Function short-circuits on first truthy return value. * * @param ?callable(T, int, static): bool $callback * @return bool @@ -120,6 +120,106 @@ public function some(callable $callback = null): bool return false; } + /** + * Returns the first element of the collection that matches the given callback. + * If no callback is given the first element in the collection is returned. + * Throws exception if collection is empty or the given callback was never satisfied. + * + * @param ?callable(T, int, static): bool $callback + * @return T + * @throws InvalidArgumentException + */ + public function first(callable $callback = null) + { + if ($this->items === []) { + throw new InvalidArgumentException('No items in collection'); + } + + foreach ($this->items as $index => $item) { + if ($callback === null || $callback($item, $index, $this)) { + return $item; + } + } + + throw new InvalidArgumentException('No item found in collection that satisfies first callback'); + } + + /** + * Returns the first element of the collection that matches the given callback. + * If no callback is given the first element in the collection is returned. + * If the collection is empty the given fallback value is returned instead. + * + * @template U of T|mixed + * @param ?callable(T, int, static): bool $callback + * @param U $fallbackValue + * @return U + * @throws InvalidArgumentException + */ + public function firstOr(callable $callback = null, mixed $fallbackValue = null) + { + if ($this->items === []) { + return $fallbackValue; + } + + foreach ($this->items as $index => $item) { + if ($callback === null || $callback($item, $index, $this)) { + return $item; + } + } + + return $fallbackValue; + } + + /** + * Returns the last element of the collection that matches the given callback. + * If no callback is given the last element in the collection is returned. + * Throws exception if collection is empty or the given callback was never satisfied. + * + * @param ?callable(T, int, static): bool $callback + * @return T + * @throws InvalidArgumentException + */ + public function last(callable $callback = null) + { + if ($this->items === []) { + throw new InvalidArgumentException('No items in collection'); + } + + foreach (array_reverse($this->items) as $index => $item) { + if ($callback === null || $callback($item, $index, $this)) { + return $item; + } + } + + throw new InvalidArgumentException('No item found in collection that satisfies last callback'); + } + + /** + * Returns the last element of the collection that matches the given callback. + * If no callback is given the last element in the collection is returned. + * If the collection is empty the given fallback value is returned instead. + * + * @template U of T|mixed + * @param ?callable(T, int, static): bool $callback + * @param U $fallbackValue + * @return U + * @throws InvalidArgumentException + */ + public function lastOr(callable $callback = null, mixed $fallbackValue = null) + { + if ($this->items === []) { + return $fallbackValue; + } + + foreach (array_reverse($this->items) as $index => $item) { + if ($callback === null || $callback($item, $index, $this)) { + return $item; + } + } + + return $fallbackValue; + } + /** * Add one or more items to the collection. It **does not** modify the * current collection, but returns a new one. diff --git a/tests/Domain/CollectionTest.php b/tests/Domain/CollectionTest.php index b154684..0972163 100644 --- a/tests/Domain/CollectionTest.php +++ b/tests/Domain/CollectionTest.php @@ -7,6 +7,7 @@ use ArrayIterator; use Assert; use GeekCell\Ddd\Domain\Collection; +use InvalidArgumentException; use PHPUnit\Framework\TestCase; /** @@ -431,4 +432,98 @@ public function testSomeShortCircuitsOnFirstFalsyValue(): void return true; }); } + + public function testFirst(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + + $this->assertSame(1, $collection->first()); + } + + public function testFirstThrowsExceptionOnEmptyCollection(): void + { + $collection = new Collection([]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No items in collection'); + $collection->first(); + } + + public function testFirstThrowsExceptionIfCallbackIsNeverSatisfied(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No item found in collection that satisfies first callback'); + $collection->first(static fn () => false); + } + + public function testFirstOrReturnsFirstValueInCollectionIfNoCallbackIsGiven(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->assertSame(1, $collection->firstOr()); + } + + public function testFirstOrReturnsFirstValueThatSatisfiesCallback(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->assertSame(6, $collection->firstOr(static fn ($item) => $item > 5)); + } + + public function testFirstOrReturnsFallbackValueIfCallbackIsNeverSatisfied(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->assertSame(-1, $collection->firstOr(static fn ($item) => $item > 10, -1)); + } + + public function testLast(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + + $this->assertSame(10, $collection->last()); + } + + public function testLastThrowsExceptionOnEmptyCollection(): void + { + $collection = new Collection([]); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No items in collection'); + $collection->last(); + } + + public function testLastThrowsExceptionIfCallbackIsNeverSatisfied(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('No item found in collection that satisfies last callback'); + $collection->last(static fn () => false); + } + + public function testLastOrReturnsLastValueInCollectionIfNoCallbackIsGiven(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->assertSame(10, $collection->lastOr()); + } + + public function testLastOrReturnsLastValueThatSatisfiesCallback(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->assertSame(10, $collection->lastOr(static fn ($item) => $item > 5)); + } + + public function testLastOrReturnsFallbackValueIfCallbackIsNeverSatisfied(): void + { + $items = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; + $collection = new Collection($items); + $this->assertSame(-1, $collection->lastOr(static fn ($item) => $item > 10, -1)); + } }