* * 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\Internal\callback; use function Kadet\Functional\Collections\Internal\extremum; use function Kadet\Functional\Collections\Internal\firstOf; use function Kadet\Functional\Collections\Internal\reversedEntries; /** * 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 = mixed * @template TValue = mixed * * @param (callable(TValue, TKey): mixed)|null $predicate * * @return ($predicate is null * ? \Closure(iterable): ?V * : \Closure(iterable): ?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 = mixed * @template TValue = mixed * * @param (callable(TValue, TKey): mixed)|null $predicate * * @return ($predicate is null * ? \Closure(iterable): ?V * : \Closure(iterable): ?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 = mixed * @template TValue = mixed * * @param callable(TValue, TKey): mixed $predicate * * @return \Closure(iterable): ?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`). * * @return \Closure(iterable): 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 = mixed * @template TValue = mixed * * @param callable(TValue, TKey): mixed $predicate * * @return \Closure(iterable): 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 = mixed * @template TValue = mixed * * @param callable(TValue, TKey): mixed $predicate * * @return \Closure(iterable): bool */ function some(callable $predicate): \Closure { return any($predicate); } /** * Whether every value satisfies `$predicate($value, $key)`. True for an empty collection. * * @template TKey = mixed * @template TValue = mixed * * @param callable(TValue, TKey): mixed $predicate * * @return \Closure(iterable): 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): 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): 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. * * With a selector this returns the selected value, not the element it was selected from. * * @template TKey = mixed * @template TValue = mixed * @template TSelected = mixed * * @param (callable(TValue, TKey): TSelected)|null $selector * * @return ($selector is null * ? \Closure(iterable): ?V * : \Closure(iterable): ?TSelected) */ 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. * * With a selector this returns the selected value, not the element it was selected from. * * @template TKey = mixed * @template TValue = mixed * @template TSelected = mixed * * @param (callable(TValue, TKey): TSelected)|null $selector * * @return ($selector is null * ? \Closure(iterable): ?V * : \Closure(iterable): ?TSelected) */ 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`. * * Internal functions (e.g. `max(...)`) and functions declaring two parameters get only `($carry, $value)`. * * @template TKey = mixed * @template TValue = mixed * @template TCarry = mixed * * @param callable(TCarry, TValue, TKey): TCarry $fn * @param TCarry $initial * * @return \Closure(iterable): TCarry */ function reduce(callable $fn, mixed $initial = null): \Closure { // The reducer takes the carry first: an internal one, or one declaring two parameters, gets ($carry, $value). $fn = callback($fn, 2); 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 = mixed * @template TValue = mixed * * @param (callable(TValue, TKey): (int|float))|null $selector * * @return \Closure(iterable): (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): 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); }; }