* * Full license available in separate LICENSE file */ use Ds\Map; use Ds\Seq; use Kadet\Functional as f; use Kadet\Functional\Collections as c; use Kadet\Functional\Exception\EmptyCollectionException; class QueriesTest extends \PHPUnit\Framework\TestCase { public function testFirstReturnsNullOnEmpty() { $this->assertNull(c\first()([])); $this->assertNull(c\first(fn ($v) => $v > 5)([1, 2])); } public function testFirstRequiredThrowsWithMessage() { $this->expectException(EmptyCollectionException::class); $this->expectExceptionMessage('first(): no element matched'); c\first(fn ($v) => $v > 5, required: true)([1, 2]); } public function testFirstRequiredWithoutPredicateSaysEmpty() { $this->expectException(EmptyCollectionException::class); $this->expectExceptionMessage('first(): collection is empty'); c\first(required: true)([]); } public function testFirstRequiredAcceptsNullMatch() { $this->assertNull(c\first(fn ($v) => $v === null, required: true)([1, null])); } public function testFirstFalsyValueFound() { $this->assertSame(0, c\first()([0])); } public function testFirstStopsEarly() { $gen = (static function () { yield 1; throw new \LogicException('consumed too far'); })(); $this->assertSame(1, c\first()($gen)); } public function testFirstPassesKeyToPredicate() { $this->assertSame(2, c\first(fn ($v, $k) => $k === 'b')(['a' => 1, 'b' => 2])); $this->assertSame(2, c\first(fn ($v, $k) => $k === 'b')(new Map(['a' => 1, 'b' => 2]))); } public function testFindIsFirstWithPredicate() { $this->assertSame(2, c\find(fn ($v) => $v > 1)([1, 2, 3])); $this->expectException(EmptyCollectionException::class); $this->expectExceptionMessage('find(): no element matched'); c\find(fn ($v) => $v > 5, required: true)([1, 2, 3]); } public function testLastOnArrayAndPredicate() { $this->assertSame(3, c\last()([1, 2, 3])); $this->assertSame(2, c\last(fn ($v) => $v < 3)([1, 2, 3])); $this->assertNull(c\last()([])); } public function testLastOnGenerator() { $gen = (static function () { yield 'a' => 1; yield 'b' => 2; })(); $this->assertSame(2, c\last()($gen)); } public function testLastOnSeqAndSet() { $this->assertSame(3, c\last()(new Seq([1, 2, 3]))); $this->assertSame(2, c\last(fn ($v, $k) => $k < 2)(new Seq([1, 2, 3]))); $this->assertSame(3, c\last()(new \Ds\Set([1, 2, 3]))); $this->assertNull(c\last()(new Seq())); } public function testLastOnMapReturnsValue() { $this->assertSame('y', c\last()(new Map(['a' => 'x', 'b' => 'y']))); $this->assertSame('x', c\last(fn ($v, $k) => $k === 'a')(new Map(['a' => 'x', 'b' => 'y']))); } public function testLastRequired() { $this->assertNull(c\last(fn ($v) => $v === null, required: true)([null, 1])); $this->expectException(EmptyCollectionException::class); $this->expectExceptionMessage('last(): collection is empty'); c\last(required: true)([]); } public function testLastRequiredNoMatchMessage() { $this->expectException(EmptyCollectionException::class); $this->expectExceptionMessage('last(): no element matched'); c\last(fn ($v) => false, required: true)([1]); } public function testContainsIsStrict() { $this->assertFalse(c\contains('1')([1])); $this->assertTrue(c\contains(1)([3, 1])); $this->assertTrue(c\contains(null)([1, null])); } public function testContainsUsesDsKey() { $key = new class (1) implements \Ds\Key { public function __construct(public int $id) {} public function equals(mixed $obj): bool { return $obj->id === $this->id; } public function hash(): mixed { return $this->id; } }; $this->assertTrue(c\contains($key)([new $key(1)])); $this->assertFalse(c\contains($key)([new $key(2)])); } public function testAnySomeEvery() { $this->assertTrue(c\any(fn ($v) => $v > 2)([1, 3])); $this->assertFalse(c\any(fn ($v) => $v > 5)([1, 3])); $this->assertFalse(c\any(fn ($v) => true)([])); $this->assertTrue(c\some(fn ($v) => $v > 2)([1, 3])); $this->assertTrue(c\every(fn ($v) => $v > 0)([1, 3])); $this->assertFalse(c\every(fn ($v) => $v > 1)([1, 3])); } public function testEveryOnEmptyIsTrue() { $this->assertTrue(c\every(fn ($v) => false)([])); } public function testAnyStopsEarly() { $gen = (static function () { yield 1; throw new \LogicException('consumed too far'); })(); $this->assertTrue(c\any(fn ($v) => $v === 1)($gen)); } public function testCountAndIsEmpty() { $gen = (static function () { yield 1; yield 2; })(); $this->assertSame(2, c\count()($gen)); $this->assertSame(3, c\count()(new Seq([1, 2, 3]))); $this->assertSame(0, c\count()([])); $this->assertTrue(c\isEmpty()([])); $this->assertFalse(c\isEmpty()(new Seq([0]))); $this->assertTrue(c\isEmpty()(new Map())); } public function testMinMax() { $this->assertSame(1, c\min()([3, 1, 2])); $this->assertSame(3, c\max()([3, 1, 2])); $this->assertNull(c\min()([])); $this->assertNull(c\max()([])); } public function testMinMaxTiesKeepFirstExtremum() { $a = (object) ['x' => 1]; $b = (object) ['x' => 1]; $this->assertSame($a, c\min()([$a, $b])); $this->assertSame($a, c\max()([$a, $b])); $this->assertSame(1, c\max()([1, 1.0])); $this->assertSame(1.0, c\min()([1.0, 1])); } public function testMinWithSelector() { $this->assertSame(1, c\min(fn ($v) => strlen($v))(['ccc', 'a', 'bb'])); $this->assertSame(3, c\max(fn ($v) => strlen($v))(['ccc', 'a', 'bb'])); } public function testReduce() { $this->assertSame(6, c\reduce(fn ($carry, $v) => $carry + $v, 0)([1, 2, 3])); $this->assertSame('a:1,b:2,', c\reduce(fn ($carry, $v, $k) => "$carry$k:$v,", '')(['a' => 1, 'b' => 2])); $this->assertNull(c\reduce(fn ($carry, $v) => $v)([])); } public function testReduceWithInternalFunctionGetsCarryAndValueOnly() { $this->assertSame(2, c\reduce(max(...), 0)([5 => 1, 9 => 2])); } public function testReduceWithDecoratedTwoParameterClosureGetsCarryAndValueOnly() { $calls = []; $reducer = f\memoize(function ($carry, $v) use (&$calls) { $calls[] = func_get_args(); return $carry + $v; }); $this->assertSame(3, c\reduce($reducer, 0)(['a' => 1, 'b' => 2])); $this->assertSame([[0, 1], [1, 2]], $calls); } public function testSum() { $this->assertSame(6, c\sum()([1, 2, 3])); $this->assertSame(3.5, c\sum()([1, 2.5])); $this->assertSame(4, c\sum(fn ($v) => strlen($v))(['ab', 'cd'])); } public function testSumEmptyIsZero() { $this->assertSame(0, c\sum()([])); } public function testJoin() { $this->assertSame('a, b', c\join(', ')(['a', 'b'])); $this->assertSame('12', c\join()(new Seq([1, 2]))); $this->assertSame('', c\join(',')([])); } }