194 lines
6.7 KiB
PHP
194 lines
6.7 KiB
PHP
<?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])));
|
|
}
|
|
}
|