344 lines
8.9 KiB
PHP
344 lines
8.9 KiB
PHP
<?php
|
|
/**
|
|
* Copyright 2026 Kacper Donat
|
|
*
|
|
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
|
*
|
|
* Full license available in separate LICENSE file
|
|
*/
|
|
|
|
/**
|
|
* Collection queries.
|
|
*
|
|
* Every function returns a closure that takes any iterable (generators included) and gives a single result, so they
|
|
* end a pipeline, eager or lazy. They stop reading as soon as the answer is known.
|
|
*
|
|
* Callbacks receive `($value, $key)`, also for `Ds\Map`. Inside this namespace `count`, `min`, `max` and `join`
|
|
* shadow the PHP builtins, so library code uses `\count()` and friends.
|
|
*/
|
|
|
|
namespace Kadet\Functional\Collections;
|
|
|
|
use Kadet\Functional\Exception\EmptyCollectionException;
|
|
|
|
use function Kadet\Functional\Collections\Internal\callback;
|
|
use function Kadet\Functional\Collections\Internal\extremum;
|
|
use function Kadet\Functional\Collections\Internal\firstOf;
|
|
use function Kadet\Functional\Collections\Internal\reversedEntries;
|
|
use function Kadet\Functional\compile;
|
|
|
|
/**
|
|
* Returns the first value for which `$predicate($value, $key)` is truthy, or the first value without a predicate.
|
|
*
|
|
* Returns null when nothing matches; with `$required` it throws instead, so a found `null` stays distinguishable.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param (callable(TValue, TKey): mixed)|null $predicate
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
|
*
|
|
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
|
*/
|
|
function first(?callable $predicate = null, bool $required = false): \Closure
|
|
{
|
|
return firstOf('first', $predicate, $required);
|
|
}
|
|
|
|
/**
|
|
* Returns the last value for which `$predicate($value, $key)` is truthy, or the last value without a predicate.
|
|
*
|
|
* Arrays and php-ds containers are read from the end, other iterables are read to the end. Returns null when
|
|
* nothing matches; with `$required` it throws instead.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param (callable(TValue, TKey): mixed)|null $predicate
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
|
*
|
|
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
|
*/
|
|
function last(?callable $predicate = null, bool $required = false): \Closure
|
|
{
|
|
$predicate = $predicate === null ? null : callback($predicate);
|
|
|
|
return static function (iterable $data) use ($predicate, $required) {
|
|
$reversible = is_array($data) || $data instanceof \Ds\Seq || $data instanceof \Ds\Set
|
|
|| $data instanceof \Ds\Map;
|
|
|
|
if ($reversible) {
|
|
foreach (reversedEntries($data) as [$key, $value]) {
|
|
if ($predicate === null || $predicate($value, $key)) {
|
|
return $value;
|
|
}
|
|
}
|
|
} else {
|
|
$found = false;
|
|
$result = null;
|
|
foreach ($data as $key => $value) {
|
|
if ($predicate === null || $predicate($value, $key)) {
|
|
$found = true;
|
|
$result = $value;
|
|
}
|
|
}
|
|
|
|
if ($found) {
|
|
return $result;
|
|
}
|
|
}
|
|
|
|
if ($required) {
|
|
throw EmptyCollectionException::noMatch('last', $predicate !== null);
|
|
}
|
|
|
|
return null;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Alias of {@see first()} with a mandatory predicate: returns the first value matching `$predicate($value, $key)`.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param callable(TValue, TKey): mixed $predicate
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): ?TValue
|
|
*
|
|
* @throws EmptyCollectionException when `$required` is set and nothing matched
|
|
*/
|
|
function find(callable $predicate, bool $required = false): \Closure
|
|
{
|
|
return firstOf('find', $predicate, $required);
|
|
}
|
|
|
|
/**
|
|
* Whether the collection holds `$value`, compared strictly (`Ds\Key::equals` when `$value` implements `Ds\Key`).
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): bool
|
|
*/
|
|
function contains(mixed $value): \Closure
|
|
{
|
|
$isKey = interface_exists(\Ds\Key::class) && $value instanceof \Ds\Key;
|
|
|
|
return static function (iterable $data) use ($value, $isKey): bool {
|
|
foreach ($data as $item) {
|
|
if ($isKey ? $item instanceof \Ds\Key && $value->equals($item) : $item === $value) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether any value satisfies `$predicate($value, $key)`. False for an empty collection.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param callable(TValue, TKey): mixed $predicate
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): bool
|
|
*/
|
|
function any(callable $predicate): \Closure
|
|
{
|
|
$predicate = callback($predicate);
|
|
|
|
return static function (iterable $data) use ($predicate): bool {
|
|
foreach ($data as $key => $value) {
|
|
if ($predicate($value, $key)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Alias of {@see any()}.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param callable(TValue, TKey): mixed $predicate
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): bool
|
|
*/
|
|
function some(callable $predicate): \Closure
|
|
{
|
|
return any($predicate);
|
|
}
|
|
|
|
/**
|
|
* Whether every value satisfies `$predicate($value, $key)`. True for an empty collection.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param callable(TValue, TKey): mixed $predicate
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): bool
|
|
*/
|
|
function every(callable $predicate): \Closure
|
|
{
|
|
$predicate = callback($predicate);
|
|
|
|
return static function (iterable $data) use ($predicate): bool {
|
|
foreach ($data as $key => $value) {
|
|
if (!$predicate($value, $key)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Counts the values. Containers that know their size are not iterated.
|
|
*
|
|
* @return \Closure(iterable<mixed, mixed>): int
|
|
*/
|
|
function count(): \Closure
|
|
{
|
|
return static function (iterable $data): int {
|
|
if (is_array($data) || $data instanceof \Countable) {
|
|
return \count($data);
|
|
}
|
|
|
|
$count = 0;
|
|
foreach ($data as $_) {
|
|
$count++;
|
|
}
|
|
|
|
return $count;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Whether the collection has no values. Reads at most one value.
|
|
*
|
|
* @return \Closure(iterable<mixed, mixed>): bool
|
|
*/
|
|
function isEmpty(): \Closure
|
|
{
|
|
return static function (iterable $data): bool {
|
|
if (is_array($data) || $data instanceof \Countable) {
|
|
return \count($data) === 0;
|
|
}
|
|
|
|
foreach ($data as $_) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The smallest value (compared with `<=>`), or the smallest of `$selector($value, $key)` when given. Null when empty.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param (callable(TValue, TKey): mixed)|null $selector
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): mixed
|
|
*/
|
|
function min(?callable $selector = null): \Closure
|
|
{
|
|
return extremum(-1, $selector);
|
|
}
|
|
|
|
/**
|
|
* The largest value (compared with `<=>`), or the largest of `$selector($value, $key)` when given. Null when empty.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param (callable(TValue, TKey): mixed)|null $selector
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): mixed
|
|
*/
|
|
function max(?callable $selector = null): \Closure
|
|
{
|
|
return extremum(1, $selector);
|
|
}
|
|
|
|
/**
|
|
* Folds the collection into a single value with `$fn($carry, $value, $key)`, starting from `$initial`.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
* @template TCarry
|
|
*
|
|
* @param callable(TCarry, TValue, TKey): TCarry $fn
|
|
* @param TCarry $initial
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): TCarry
|
|
*/
|
|
function reduce(callable $fn, mixed $initial = null): \Closure
|
|
{
|
|
// The reducer takes the carry first, so it is compiled without the ($value, $key) adaptation.
|
|
$fn = compile($fn);
|
|
|
|
return static function (iterable $data) use ($fn, $initial) {
|
|
$carry = $initial;
|
|
foreach ($data as $key => $value) {
|
|
$carry = $fn($carry, $value, $key);
|
|
}
|
|
|
|
return $carry;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Sums the values, or `$selector($value, $key)` when given. An empty collection sums to 0.
|
|
*
|
|
* @template TKey
|
|
* @template TValue
|
|
*
|
|
* @param (callable(TValue, TKey): int|float)|null $selector
|
|
*
|
|
* @return \Closure(iterable<TKey, TValue>): (int|float)
|
|
*/
|
|
function sum(?callable $selector = null): \Closure
|
|
{
|
|
$selector = $selector === null ? static fn ($value) => $value : callback($selector);
|
|
|
|
return static function (iterable $data) use ($selector): int|float {
|
|
$sum = 0;
|
|
foreach ($data as $key => $value) {
|
|
$sum += $selector($value, $key);
|
|
}
|
|
|
|
return $sum;
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Joins the values into a string with `$glue`.
|
|
*
|
|
* @return \Closure(iterable<mixed, string|\Stringable|int|float>): string
|
|
*/
|
|
function join(string $glue = ''): \Closure
|
|
{
|
|
return static function (iterable $data) use ($glue): string {
|
|
$parts = [];
|
|
foreach ($data as $value) {
|
|
$parts[] = (string) $value;
|
|
}
|
|
|
|
return implode($glue, $parts);
|
|
};
|
|
}
|