functional-php/tests/Collections/LazyTest.php
2026-10-27 22:26:35 +01:00

171 lines
6.1 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 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 'keys' => [fn () => lazy\keys(), fn () => c\keys()];
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 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 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 testStepCanBeReusedAcrossInputs()
{
$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])));
}
}