Skip to content

Commit

Permalink
feat: add first, firstOr, last, lastOr functions to collection
Browse files Browse the repository at this point in the history
  • Loading branch information
Bl00D4NGEL committed Jan 5, 2024
1 parent a18d46f commit fa0e4ad
Show file tree
Hide file tree
Showing 2 changed files with 199 additions and 4 deletions.
108 changes: 104 additions & 4 deletions src/Domain/Collection.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> $items
* @param class-string<T>|null $itemType
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down
95 changes: 95 additions & 0 deletions tests/Domain/CollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use ArrayIterator;
use Assert;
use GeekCell\Ddd\Domain\Collection;
use InvalidArgumentException;
use PHPUnit\Framework\TestCase;

/**
Expand Down Expand Up @@ -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));
}
}

0 comments on commit fa0e4ad

Please sign in to comment.