add lazy collection transforms
This commit is contained in:
parent
b836bc5e8f
commit
364b97deaa
@ -27,6 +27,7 @@
|
||||
"./src/Collections/Internal/functions.php",
|
||||
"./src/Collections/Internal/containers.php",
|
||||
"./src/Collections/Internal/queries.php",
|
||||
"./src/Collections/Lazy/transforms.php",
|
||||
"./src/Collections/transforms.php",
|
||||
"./src/Collections/collectors.php",
|
||||
"./src/Collections/queries.php"
|
||||
|
||||
356
src/Collections/Lazy/transforms.php
Normal file
356
src/Collections/Lazy/transforms.php
Normal file
@ -0,0 +1,356 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 2026 Kacper Donat
|
||||
*
|
||||
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||
*
|
||||
* Full license available in separate LICENSE file
|
||||
*/
|
||||
|
||||
/**
|
||||
* Lazy collection transforms.
|
||||
*
|
||||
* Every function returns a closure that takes any iterable and returns a generator, so nothing is done until the
|
||||
* result is consumed and a chain walks its source once. Keys are preserved, except for {@see values()},
|
||||
* {@see keys()}, {@see flatMap()}, {@see prepend()}, {@see append()} and {@see concat()}, which yield sequential keys.
|
||||
* End a chain with a collector or query from `Kadet\Functional\Collections`, e.g. `toArray()`.
|
||||
*
|
||||
* The eager transforms in `Kadet\Functional\Collections` are built on these, so both flavours behave the same.
|
||||
* Callbacks receive `($value, $key)`. There are no sorting, grouping or keyed `merge` functions here, as those
|
||||
* need the whole input.
|
||||
*/
|
||||
|
||||
namespace Kadet\Functional\Collections\Lazy;
|
||||
|
||||
use function Kadet\Functional\Collections\Internal\callback;
|
||||
use function Kadet\Functional\Collections\Internal\entries;
|
||||
use function Kadet\Functional\Collections\Internal\seen;
|
||||
use function Kadet\Functional\Collections\Internal\unpairs;
|
||||
|
||||
/**
|
||||
* Transforms each value with `$fn($value, $key)`, keeping the keys.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @template TResult
|
||||
*
|
||||
* @param callable(TValue, TKey): TResult $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<TKey, TResult>
|
||||
*/
|
||||
function map(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): \Generator {
|
||||
foreach ($input as $key => $value) {
|
||||
yield $key => $fn($value, $key);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Keeps the values for which `$fn($value, $key)` is truthy, or the truthy values when `$fn` is null.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param (callable(TValue, TKey): mixed)|null $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function filter(?callable $fn = null): \Closure
|
||||
{
|
||||
$fn = $fn === null ? static fn ($value) => $value : callback($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): \Generator {
|
||||
foreach ($input as $key => $value) {
|
||||
if ($fn($value, $key)) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the values for which `$fn($value, $key)` is truthy. The inverse of {@see filter()}.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function reject(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): \Generator {
|
||||
foreach ($input as $key => $value) {
|
||||
if (!$fn($value, $key)) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps each value to an iterable with `$fn($value, $key)` and flattens the results one level, with sequential keys.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
* @template TResult
|
||||
*
|
||||
* @param callable(TValue, TKey): iterable<mixed, TResult> $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<int, TResult>
|
||||
*/
|
||||
function flatMap(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): \Generator {
|
||||
foreach ($input as $key => $value) {
|
||||
$result = $fn($value, $key);
|
||||
|
||||
if (!is_iterable($result)) {
|
||||
throw new \UnexpectedValueException(sprintf(
|
||||
'flatMap(): the callback must return an iterable, got %s.',
|
||||
get_debug_type($result),
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($result as $item) {
|
||||
yield $item;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the keys, yielding the values with sequential keys.
|
||||
*
|
||||
* @return \Closure<TValue>(iterable<mixed, TValue>): \Generator<int, TValue>
|
||||
*/
|
||||
function values(): \Closure
|
||||
{
|
||||
return static function (iterable $input): \Generator {
|
||||
foreach ($input as $value) {
|
||||
yield $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Yields the keys as values, with sequential keys.
|
||||
*
|
||||
* @return \Closure<TKey>(iterable<TKey, mixed>): \Generator<int, TKey>
|
||||
*/
|
||||
function keys(): \Closure
|
||||
{
|
||||
return static function (iterable $input): \Generator {
|
||||
foreach ($input as $key => $_) {
|
||||
yield $key;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes duplicate values, keeping the first occurrence. Values are compared strictly, objects by identity.
|
||||
*
|
||||
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function unique(): \Closure
|
||||
{
|
||||
return uniqueBy(static fn ($value) => $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes values whose `$selector($value, $key)` result was already seen, keeping the first occurrence.
|
||||
* Selected values are compared strictly, objects by identity.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $selector
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function uniqueBy(callable $selector): \Closure
|
||||
{
|
||||
$selector = callback($selector);
|
||||
|
||||
return static function (iterable $input) use ($selector): \Generator {
|
||||
$seen = seen();
|
||||
|
||||
foreach ($input as $key => $value) {
|
||||
if (!$seen($selector($value, $key))) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes `$length` values (all remaining when null) starting at position `$offset`, like `array_slice()`.
|
||||
*
|
||||
* Negative `$offset` counts from the end, negative `$length` stops that many values before the end. With
|
||||
* non-negative arguments the source is consumed only as far as needed; a negative argument needs the whole input.
|
||||
*
|
||||
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function slice(int $offset, ?int $length = null): \Closure
|
||||
{
|
||||
return static function (iterable $input) use ($offset, $length): \Generator {
|
||||
if ($offset < 0 || ($length !== null && $length < 0)) {
|
||||
yield from unpairs(array_slice(entries($input), $offset, $length));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$position = 0;
|
||||
$taken = 0;
|
||||
foreach ($input as $key => $value) {
|
||||
if ($position++ < $offset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $key => $value;
|
||||
|
||||
if (++$taken === $length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes the first `$n` values. See {@see slice()}.
|
||||
*
|
||||
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function take(int $n): \Closure
|
||||
{
|
||||
if ($n < 0) {
|
||||
throw new \InvalidArgumentException(sprintf('take(): count must be >= 0, got %d', $n));
|
||||
}
|
||||
|
||||
return slice(0, $n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Skips the first `$n` values. See {@see slice()}.
|
||||
*
|
||||
* @return \Closure<TKey, TValue>(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function skip(int $n): \Closure
|
||||
{
|
||||
if ($n < 0) {
|
||||
throw new \InvalidArgumentException(sprintf('skip(): count must be >= 0, got %d', $n));
|
||||
}
|
||||
|
||||
return slice($n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts `$values` before the values of the input, with sequential keys.
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @param TValue ...$values
|
||||
*
|
||||
* @return \Closure(iterable<mixed, TValue>): \Generator<int, TValue>
|
||||
*/
|
||||
function prepend(mixed ...$values): \Closure
|
||||
{
|
||||
return static function (iterable $input) use ($values): \Generator {
|
||||
foreach ($values as $value) {
|
||||
yield $value;
|
||||
}
|
||||
|
||||
foreach ($input as $value) {
|
||||
yield $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts `$values` after the values of the input, with sequential keys.
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @param TValue ...$values
|
||||
*
|
||||
* @return \Closure(iterable<mixed, TValue>): \Generator<int, TValue>
|
||||
*/
|
||||
function append(mixed ...$values): \Closure
|
||||
{
|
||||
return concat($values);
|
||||
}
|
||||
|
||||
/**
|
||||
* Puts the values of `$other` after the values of the input, with sequential keys.
|
||||
*
|
||||
* @template TValue
|
||||
*
|
||||
* @param iterable<mixed, TValue> $other
|
||||
*
|
||||
* @return \Closure(iterable<mixed, TValue>): \Generator<int, TValue>
|
||||
*/
|
||||
function concat(iterable $other): \Closure
|
||||
{
|
||||
return static function (iterable $input) use ($other): \Generator {
|
||||
foreach ($input as $value) {
|
||||
yield $value;
|
||||
}
|
||||
|
||||
foreach ($other as $value) {
|
||||
yield $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `$fn($value, $key)` for each value, for its side effects, and yields every element through unchanged.
|
||||
*
|
||||
* The return value of `$fn` is ignored.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function each(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): \Generator {
|
||||
foreach ($input as $key => $value) {
|
||||
$fn($value, $key);
|
||||
|
||||
yield $key => $value;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias of {@see each()}.
|
||||
*
|
||||
* @template TKey
|
||||
* @template TValue
|
||||
*
|
||||
* @param callable(TValue, TKey): mixed $fn
|
||||
*
|
||||
* @return \Closure(iterable<TKey, TValue>): \Generator<TKey, TValue>
|
||||
*/
|
||||
function tap(callable $fn): \Closure
|
||||
{
|
||||
return each($fn);
|
||||
}
|
||||
@ -20,11 +20,11 @@
|
||||
|
||||
namespace Kadet\Functional\Collections;
|
||||
|
||||
use Kadet\Functional\Collections\Lazy as lazy;
|
||||
use function Kadet\Functional\Collections\Internal\callback;
|
||||
use function Kadet\Functional\Collections\Internal\entries;
|
||||
use function Kadet\Functional\Collections\Internal\keyedLike;
|
||||
use function Kadet\Functional\Collections\Internal\listLike;
|
||||
use function Kadet\Functional\Collections\Internal\seen;
|
||||
use function Kadet\Functional\Collections\Internal\seqOf;
|
||||
use function Kadet\Functional\Collections\Internal\unpairs;
|
||||
use function Kadet\Functional\compile;
|
||||
@ -44,15 +44,9 @@ use function Kadet\Functional\compile;
|
||||
*/
|
||||
function map(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
$lazy = lazy\map($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): iterable {
|
||||
return keyedLike($input, (static function () use ($input, $fn) {
|
||||
foreach ($input as $key => $value) {
|
||||
yield $key => $fn($value, $key);
|
||||
}
|
||||
})());
|
||||
};
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -69,17 +63,9 @@ function map(callable $fn): \Closure
|
||||
*/
|
||||
function filter(?callable $fn = null): \Closure
|
||||
{
|
||||
$fn = $fn === null ? static fn ($value) => $value : callback($fn);
|
||||
$lazy = lazy\filter($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): iterable {
|
||||
return keyedLike($input, (static function () use ($input, $fn) {
|
||||
foreach ($input as $key => $value) {
|
||||
if ($fn($value, $key)) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
})());
|
||||
};
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -94,17 +80,9 @@ function filter(?callable $fn = null): \Closure
|
||||
*/
|
||||
function reject(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
$lazy = lazy\reject($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): iterable {
|
||||
return keyedLike($input, (static function () use ($input, $fn) {
|
||||
foreach ($input as $key => $value) {
|
||||
if (!$fn($value, $key)) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
})());
|
||||
};
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -122,26 +100,9 @@ function reject(callable $fn): \Closure
|
||||
*/
|
||||
function flatMap(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
$lazy = lazy\flatMap($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): iterable {
|
||||
return listLike($input, (static function () use ($input, $fn) {
|
||||
foreach ($input as $key => $value) {
|
||||
$result = $fn($value, $key);
|
||||
|
||||
if (!is_iterable($result)) {
|
||||
throw new \UnexpectedValueException(sprintf(
|
||||
'flatMap(): the callback must return an iterable, got %s.',
|
||||
get_debug_type($result),
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($result as $item) {
|
||||
yield $item;
|
||||
}
|
||||
}
|
||||
})());
|
||||
};
|
||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -151,7 +112,9 @@ function flatMap(callable $fn): \Closure
|
||||
*/
|
||||
function values(): \Closure
|
||||
{
|
||||
return static fn (iterable $input): iterable => listLike($input, $input);
|
||||
$lazy = lazy\values();
|
||||
|
||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -161,17 +124,12 @@ function values(): \Closure
|
||||
*/
|
||||
function keys(): \Closure
|
||||
{
|
||||
return static function (iterable $input): iterable {
|
||||
if (is_array($input)) {
|
||||
return array_keys($input);
|
||||
}
|
||||
$lazy = lazy\keys();
|
||||
|
||||
$keys = [];
|
||||
foreach ($input as $key => $_) {
|
||||
$keys[] = $key;
|
||||
}
|
||||
return static function (iterable $input) use ($lazy): iterable {
|
||||
$keys = iterator_to_array($lazy($input), false);
|
||||
|
||||
return seqOf($keys);
|
||||
return is_array($input) ? $keys : seqOf($keys);
|
||||
};
|
||||
}
|
||||
|
||||
@ -198,19 +156,9 @@ function unique(): \Closure
|
||||
*/
|
||||
function uniqueBy(callable $selector): \Closure
|
||||
{
|
||||
$selector = callback($selector);
|
||||
$lazy = lazy\uniqueBy($selector);
|
||||
|
||||
return static function (iterable $input) use ($selector): iterable {
|
||||
$seen = seen();
|
||||
|
||||
return keyedLike($input, (static function () use ($input, $selector, $seen) {
|
||||
foreach ($input as $key => $value) {
|
||||
if (!$seen($selector($value, $key))) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
})());
|
||||
};
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -277,31 +225,9 @@ function sortBy(callable $selector, bool $descending = false): \Closure
|
||||
*/
|
||||
function slice(int $offset, ?int $length = null): \Closure
|
||||
{
|
||||
return static function (iterable $input) use ($offset, $length): iterable {
|
||||
if ($offset < 0 || ($length !== null && $length < 0)) {
|
||||
return keyedLike($input, unpairs(array_slice(entries($input), $offset, $length)));
|
||||
}
|
||||
$lazy = lazy\slice($offset, $length);
|
||||
|
||||
return keyedLike($input, (static function () use ($input, $offset, $length) {
|
||||
if ($length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$position = 0;
|
||||
$taken = 0;
|
||||
foreach ($input as $key => $value) {
|
||||
if ($position++ < $offset) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $key => $value;
|
||||
|
||||
if (++$taken === $length) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
})());
|
||||
};
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -311,11 +237,9 @@ function slice(int $offset, ?int $length = null): \Closure
|
||||
*/
|
||||
function take(int $n): \Closure
|
||||
{
|
||||
if ($n < 0) {
|
||||
throw new \InvalidArgumentException(sprintf('take(): count must be >= 0, got %d', $n));
|
||||
}
|
||||
$lazy = lazy\take($n);
|
||||
|
||||
return slice(0, $n);
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -325,11 +249,9 @@ function take(int $n): \Closure
|
||||
*/
|
||||
function skip(int $n): \Closure
|
||||
{
|
||||
if ($n < 0) {
|
||||
throw new \InvalidArgumentException(sprintf('skip(): count must be >= 0, got %d', $n));
|
||||
}
|
||||
$lazy = lazy\skip($n);
|
||||
|
||||
return slice($n);
|
||||
return static fn (iterable $input): iterable => keyedLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -343,10 +265,9 @@ function skip(int $n): \Closure
|
||||
*/
|
||||
function prepend(mixed ...$values): \Closure
|
||||
{
|
||||
return static fn (iterable $input): iterable => listLike($input, (static function () use ($input, $values) {
|
||||
yield from $values;
|
||||
yield from $input;
|
||||
})());
|
||||
$lazy = lazy\prepend(...$values);
|
||||
|
||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -374,10 +295,9 @@ function append(mixed ...$values): \Closure
|
||||
*/
|
||||
function concat(iterable $other): \Closure
|
||||
{
|
||||
return static fn (iterable $input): iterable => listLike($input, (static function () use ($input, $other) {
|
||||
yield from $input;
|
||||
yield from $other;
|
||||
})());
|
||||
$lazy = lazy\concat($other);
|
||||
|
||||
return static fn (iterable $input): iterable => listLike($input, $lazy($input));
|
||||
}
|
||||
|
||||
/**
|
||||
@ -415,24 +335,18 @@ function merge(iterable $other): \Closure
|
||||
*/
|
||||
function each(callable $fn): \Closure
|
||||
{
|
||||
$fn = callback($fn);
|
||||
$lazy = lazy\each($fn);
|
||||
|
||||
return static function (iterable $input) use ($fn): iterable {
|
||||
return static function (iterable $input) use ($lazy): iterable {
|
||||
$reusable = is_array($input) || $input instanceof \Ds\Seq || $input instanceof \Ds\Map || $input instanceof \Ds\Set;
|
||||
if ($reusable) {
|
||||
foreach ($input as $key => $value) {
|
||||
$fn($value, $key);
|
||||
foreach ($lazy($input) as $_) {
|
||||
}
|
||||
|
||||
return $input;
|
||||
}
|
||||
|
||||
return listLike($input, (static function () use ($input, $fn) {
|
||||
foreach ($input as $key => $value) {
|
||||
$fn($value, $key);
|
||||
yield $value;
|
||||
}
|
||||
})());
|
||||
return listLike($input, $lazy($input));
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
193
tests/Collections/LazyTest.php
Normal file
193
tests/Collections/LazyTest.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
/**
|
||||
* Copyright 2026 Kacper Donat
|
||||
*
|
||||
* @author Kacper "Kadet" Donat <kacper@kadet.net>
|
||||
*
|
||||
* Full license available in separate LICENSE file
|
||||
*/
|
||||
|
||||
use Kadet\Functional\Collections as c;
|
||||
use Kadet\Functional\Collections\Lazy as lazy;
|
||||
|
||||
class LazyTest extends \PHPUnit\Framework\TestCase
|
||||
{
|
||||
private static function gen(iterable $entries): \Generator
|
||||
{
|
||||
foreach ($entries as $key => $value) {
|
||||
yield $key => $value;
|
||||
}
|
||||
}
|
||||
|
||||
private static function naturals(): \Generator
|
||||
{
|
||||
for ($i = 1; ; $i++) {
|
||||
yield $i;
|
||||
}
|
||||
}
|
||||
|
||||
public function testLazyDoesNothingUntilConsumed()
|
||||
{
|
||||
$calls = 0;
|
||||
$spy = static function ($value) use (&$calls) {
|
||||
$calls++;
|
||||
|
||||
return $value;
|
||||
};
|
||||
|
||||
$steps = [
|
||||
lazy\map($spy), lazy\filter($spy), lazy\reject($spy), lazy\uniqueBy($spy), lazy\each($spy), lazy\tap($spy),
|
||||
lazy\flatMap(static function ($value) use ($spy) { $spy($value); return [$value]; }),
|
||||
];
|
||||
|
||||
foreach ($steps as $step) {
|
||||
$result = $step(self::gen(['a' => 1, 'b' => 2]));
|
||||
$this->assertInstanceOf(\Generator::class, $result);
|
||||
}
|
||||
|
||||
$this->assertSame(0, $calls);
|
||||
|
||||
iterator_to_array(lazy\map($spy)(self::gen(['a' => 1, 'b' => 2])));
|
||||
$this->assertSame(2, $calls);
|
||||
}
|
||||
|
||||
public function testLazyChainSingleWalk()
|
||||
{
|
||||
$walked = 0;
|
||||
|
||||
$result = c\toArray()(lazy\take(3)(lazy\filter(static fn ($x) => $x % 2 === 0)(lazy\map(static function ($x) use (&$walked) {
|
||||
$walked++;
|
||||
|
||||
return $x * 3;
|
||||
})(self::naturals()))));
|
||||
|
||||
$this->assertSame([1 => 6, 3 => 12, 5 => 18], $result);
|
||||
$this->assertSame(6, $walked);
|
||||
}
|
||||
|
||||
public function testLazyChainWorksWithPipe()
|
||||
{
|
||||
$result = self::naturals()
|
||||
|> lazy\map(static fn ($x) => $x * 2)
|
||||
|> lazy\skip(1)
|
||||
|> lazy\take(2)
|
||||
|> c\toArray();
|
||||
|
||||
$this->assertSame([1 => 4, 2 => 6], $result);
|
||||
}
|
||||
|
||||
public static function provideEagerEquivalents(): iterable
|
||||
{
|
||||
$double = static fn ($x) => $x * 2;
|
||||
$isTwo = static fn ($x) => $x === 2;
|
||||
$inner = static fn ($x, $k) => [$x, $k];
|
||||
|
||||
yield 'map' => [fn () => lazy\map($double), fn () => c\map($double)];
|
||||
yield 'filter' => [fn () => lazy\filter($isTwo), fn () => c\filter($isTwo)];
|
||||
yield 'filter null' => [fn () => lazy\filter(), fn () => c\filter()];
|
||||
yield 'reject' => [fn () => lazy\reject($isTwo), fn () => c\reject($isTwo)];
|
||||
yield 'flatMap' => [fn () => lazy\flatMap($inner), fn () => c\flatMap($inner)];
|
||||
yield 'values' => [fn () => lazy\values(), fn () => c\values()];
|
||||
yield 'unique' => [fn () => lazy\unique(), fn () => c\unique()];
|
||||
yield 'uniqueBy' => [fn () => lazy\uniqueBy($double), fn () => c\uniqueBy($double)];
|
||||
yield 'slice' => [fn () => lazy\slice(1, 1), fn () => c\slice(1, 1)];
|
||||
yield 'slice open' => [fn () => lazy\slice(1), fn () => c\slice(1)];
|
||||
yield 'slice neg' => [fn () => lazy\slice(-2, 1), fn () => c\slice(-2, 1)];
|
||||
yield 'slice negend' => [fn () => lazy\slice(0, -1), fn () => c\slice(0, -1)];
|
||||
yield 'take' => [fn () => lazy\take(2), fn () => c\take(2)];
|
||||
yield 'skip' => [fn () => lazy\skip(2), fn () => c\skip(2)];
|
||||
yield 'prepend' => [fn () => lazy\prepend(7, 8), fn () => c\prepend(7, 8)];
|
||||
yield 'append' => [fn () => lazy\append(7, 8), fn () => c\append(7, 8)];
|
||||
yield 'concat' => [fn () => lazy\concat(['x' => 9]), fn () => c\concat(['x' => 9])];
|
||||
yield 'each' => [fn () => lazy\each($double), fn () => c\each($double)];
|
||||
yield 'tap' => [fn () => lazy\tap($double), fn () => c\tap($double)];
|
||||
}
|
||||
|
||||
#[\PHPUnit\Framework\Attributes\DataProvider('provideEagerEquivalents')]
|
||||
public function testLazyMatchesEagerResults(\Closure $lazy, \Closure $eager)
|
||||
{
|
||||
$fixture = ['a' => 1, 'b' => 2, 'c' => 2];
|
||||
|
||||
$this->assertSame($eager()($fixture), iterator_to_array($lazy()($fixture)));
|
||||
}
|
||||
|
||||
public function testKeysMatchesEager()
|
||||
{
|
||||
$fixture = ['a' => 1, 'b' => 2];
|
||||
|
||||
$this->assertSame(['a', 'b'], iterator_to_array(lazy\keys()($fixture)));
|
||||
$this->assertSame([0, 1], array_keys(iterator_to_array(lazy\keys()($fixture))));
|
||||
}
|
||||
|
||||
public function testLazyUniqueStreams()
|
||||
{
|
||||
$walked = 0;
|
||||
$source = (static function () use (&$walked) {
|
||||
foreach ([1, 1, 2, 2, 3, 4] as $value) {
|
||||
$walked++;
|
||||
yield $value;
|
||||
}
|
||||
})();
|
||||
|
||||
$generator = lazy\unique()($source);
|
||||
$this->assertSame(0, $walked);
|
||||
$this->assertSame(1, $generator->current());
|
||||
$this->assertSame(1, $walked);
|
||||
|
||||
$generator->next();
|
||||
$this->assertSame(2, $generator->current());
|
||||
$this->assertSame(3, $walked);
|
||||
}
|
||||
|
||||
public function testUniqueCanBeReusedAcrossRuns()
|
||||
{
|
||||
$step = lazy\unique();
|
||||
|
||||
$this->assertSame([1, 2], array_values(iterator_to_array($step([1, 1, 2]))));
|
||||
$this->assertSame([1, 2], array_values(iterator_to_array($step([1, 2, 2]))));
|
||||
}
|
||||
|
||||
public function testFlatMapRejectsNonIterable()
|
||||
{
|
||||
$this->expectException(\UnexpectedValueException::class);
|
||||
$this->expectExceptionMessage('flatMap(): the callback must return an iterable, got int.');
|
||||
|
||||
iterator_to_array(lazy\flatMap(static fn ($x) => $x)([1]));
|
||||
}
|
||||
|
||||
public function testTakeAndSkipRejectNegativeWhenBuilt()
|
||||
{
|
||||
try {
|
||||
lazy\take(-1);
|
||||
$this->fail('take(-1) should throw');
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
$this->assertSame('take(): count must be >= 0, got -1', $e->getMessage());
|
||||
}
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('skip(): count must be >= 0, got -1');
|
||||
lazy\skip(-1);
|
||||
}
|
||||
|
||||
public function testTakeStopsPullingTheSource()
|
||||
{
|
||||
$this->assertSame([1, 2], array_values(iterator_to_array(lazy\take(2)(self::naturals()))));
|
||||
}
|
||||
|
||||
public function testEachYieldsEverythingThroughWithKeys()
|
||||
{
|
||||
$seen = [];
|
||||
$out = iterator_to_array(lazy\each(static function ($v, $k) use (&$seen) { $seen[] = [$k, $v]; })(self::gen(['a' => 1, 'b' => 2])));
|
||||
|
||||
$this->assertSame(['a' => 1, 'b' => 2], $out);
|
||||
$this->assertSame([['a', 1], ['b', 2]], $seen);
|
||||
}
|
||||
|
||||
public function testCallbackIsResolvedOncePerStep()
|
||||
{
|
||||
$step = lazy\map(static fn ($x) => $x + 1);
|
||||
|
||||
$this->assertSame([2, 3], iterator_to_array($step([1, 2])));
|
||||
$this->assertSame([4], iterator_to_array($step([3])));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user