* * Full license available in separate LICENSE file */ use Ds\Map; use Ds\Seq; use Ds\Set; use Kadet\Functional\Collections as c; use function Kadet\Functional\partial; use const Kadet\Functional\_; class TransformsTest extends \PHPUnit\Framework\TestCase { private static function gen(iterable $entries): \Generator { foreach ($entries as $key => $value) { yield $key => $value; } } private function assertSeq(array $expected, $actual) { $this->assertInstanceOf(Seq::class, $actual); $this->assertSame($expected, $actual->toArray()); } private function assertMap(array $expected, $actual) { $this->assertInstanceOf(Map::class, $actual); $this->assertSame($expected, $actual->toArray()); } // map public function testMapOnArrayPreservesKeys() { $this->assertSame(['a' => 2, 'b' => 4], c\map(fn ($v) => $v * 2)(['a' => 1, 'b' => 2])); } public function testMapOnSeq() { $this->assertSeq([2, 4], c\map(fn ($v) => $v * 2)(new Seq([1, 2]))); } public function testMapOnMapPassesValueThenKey() { $this->assertEquals(new Map(['a' => 'a:1']), c\map(fn ($v, $k) => "$k:$v")(new Map(['a' => 1]))); } public function testMapOnGeneratorGivesSeq() { $this->assertSeq([2, 4], c\map(fn ($v) => $v * 2)(self::gen([1, 2]))); } public function testMapOnGeneratorPassesGeneratorKeys() { $this->assertSeq(['x:1', 'y:2'], c\map(fn ($v, $k) => "$k:$v")(self::gen(['x' => 1, 'y' => 2]))); } public function testMapOnSetFollowsSetMap() { $result = c\map(fn ($v) => $v % 2)(new Set([1, 2, 3])); $this->assertInstanceOf(Set::class, $result); $this->assertSame([1, 0], $result->toArray()); } public function testMapWithInternalFunctionGetsValueOnly() { $this->assertSame(['a' => 'x'], c\map(trim(...))(['a' => ' x '])); } public function testTransformsWorkWithDecoratedCallback() { $this->assertSame(['aa'], c\map(partial(str_repeat(...), _, 2))(['a'])); } // filter / reject public function testFilterOnSeqReindexes() { $this->assertSame([1, 2], c\filter()(new Seq([0, 1, '', 2]))->toArray()); } public function testFilterOnArrayPreservesKeys() { $this->assertSame([1 => 1, 3 => 2], c\filter()([0, 1, '', 2])); } public function testFilterOnMapPassesValueThenKey() { $this->assertMap(['b' => 2], c\filter(fn ($v, $k) => $k === 'b')(new Map(['a' => 1, 'b' => 2]))); } public function testFilterOnGeneratorGivesSeq() { $this->assertSeq([2], c\filter(fn ($v) => $v % 2 === 0)(self::gen([1, 2, 3]))); } public function testFilterWithInternalPredicate() { $this->assertSame([1 => 'a'], c\filter(is_string(...))([1, 'a'])); } public function testReject() { $odd = fn ($v) => $v % 2 === 1; $this->assertSame([1 => 2], c\reject($odd)([1, 2, 3])); $this->assertSeq([2], c\reject($odd)(new Seq([1, 2, 3]))); $this->assertMap(['b' => 2], c\reject($odd)(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([2], c\reject($odd)(self::gen([1, 2, 3]))); } // flatMap public function testFlatMap() { $pair = fn ($v) => [$v, $v * 10]; $this->assertSame([1, 10, 2, 20], c\flatMap($pair)(['a' => 1, 'b' => 2])); $this->assertSeq([1, 10, 2, 20], c\flatMap($pair)(new Seq([1, 2]))); $this->assertSeq([1, 10, 2, 20], c\flatMap($pair)(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([1, 10, 2, 20], c\flatMap($pair)(self::gen([1, 2]))); } public function testFlatMapFlattensOneLevelOnly() { $this->assertSame([[1], 2], c\flatMap(fn ($v) => $v)([[[1]], [2]])); } public function testFlatMapAcceptsIterableResults() { $this->assertSame(['a', 'b', 'c'], c\flatMap(fn ($v) => new Seq(str_split($v)))(['ab', 'c'])); } // values / keys public function testValues() { $this->assertSame([1, 2], c\values()(['a' => 1, 'b' => 2])); $this->assertSeq([1, 2], c\values()(new Seq([1, 2]))); $this->assertSeq([1, 2], c\values()(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([1, 2], c\values()(self::gen(['a' => 1, 'b' => 2]))); } public function testKeys() { $this->assertSame(['a', 'b'], c\keys()(['a' => 1, 'b' => 2])); $this->assertSeq([0, 1], c\keys()(new Seq([5, 6]))); $this->assertSeq(['a', 'b'], c\keys()(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq(['a', 'b'], c\keys()(self::gen(['a' => 1, 'b' => 2]))); } // unique / uniqueBy public function testUnique() { $this->assertSame([0 => 1, 1 => '1', 3 => 2], c\unique()([1, '1', 1, 2])); $this->assertSeq([1, '1', 2], c\unique()(new Seq([1, '1', 1, 2]))); $this->assertMap(['a' => 1, 'c' => 2], c\unique()(new Map(['a' => 1, 'b' => 1, 'c' => 2]))); $this->assertSeq([1, 2], c\unique()(self::gen([1, 2, 2, 1]))); } public function testUniqueComparesObjectsByIdentity() { $a = new \stdClass(); $b = new \stdClass(); $this->assertSame([$a, $b], array_values(c\unique()([$a, $b, $a]))); } public function testUniqueComparesFloatsAndArraysStrictly() { $this->assertSame([0 => 1.0, 1 => 1, 2 => [1], 4 => ['1']], c\unique()([1.0, 1, [1], 1.0, ['1'], [1]])); } public function testUniqueByKeepsFirst() { $items = [['id' => 1, 'n' => 'a'], ['id' => 1, 'n' => 'b'], ['id' => 2, 'n' => 'c']]; $this->assertSame(['a', 'c'], array_column(c\uniqueBy(fn ($o) => $o['id'])($items), 'n')); $this->assertSame([0, 2], array_keys(c\uniqueBy(fn ($o) => $o['id'])($items))); $this->assertSame( ['a', 'c'], c\uniqueBy(fn ($o) => $o['id'])(new Seq($items))->map(fn ($o) => $o['n'])->toArray(), ); $this->assertSame(['x', 'z'], c\uniqueBy(fn ($o) => $o['id'])(new Map(['x' => $items[0], 'y' => $items[1], 'z' => $items[2]]))->keys()->toArray()); $this->assertCount(2, c\uniqueBy(fn ($o) => $o['id'])(self::gen($items))); } public function testUniqueByKeepsTemporaryObjectsDistinct() { $this->assertSame([1, 2, 3], c\uniqueBy(fn ($v) => new \stdClass())([1, 2, 3])); } // sort / sortBy public function testSort() { $this->assertSame([1 => 1, 2 => 2, 0 => 3], c\sort()([3, 1, 2])); $this->assertSeq([1, 2, 3], c\sort()(new Seq([3, 1, 2]))); $this->assertMap(['b' => 1, 'a' => 3], c\sort()(new Map(['a' => 3, 'b' => 1]))); $this->assertSeq([1, 2, 3], c\sort()(self::gen([3, 1, 2]))); } public function testSortWithComparator() { $this->assertSame([3, 2, 1], array_values(c\sort(fn ($a, $b) => $b <=> $a)([1, 3, 2]))); } public function testSortWithInternalComparatorGetsBothArguments() { $this->assertSame(['a', 'b', 'c'], array_values(c\sort(strcmp(...))(['c', 'a', 'b']))); } public function testSortIsStable() { $items = [['k' => 1, 'n' => 'a'], ['k' => 0, 'n' => 'b'], ['k' => 1, 'n' => 'c'], ['k' => 0, 'n' => 'd']]; $this->assertSame(['b', 'd', 'a', 'c'], array_column(c\sort(fn ($a, $b) => $a['k'] <=> $b['k'])($items), 'n')); $this->assertSame(['a', 'c', 'b', 'd'], array_column(c\sortBy(fn ($o) => $o['k'], descending: true)($items), 'n')); } public function testSortBy() { $len = fn ($v) => strlen($v); $this->assertSame([1 => 'a', 2 => 'bb', 0 => 'ccc'], c\sortBy($len)(['ccc', 'a', 'bb'])); $this->assertSame([0 => 'ccc', 2 => 'bb', 1 => 'a'], c\sortBy($len, true)(['ccc', 'a', 'bb'])); $this->assertSeq(['a', 'bb', 'ccc'], c\sortBy($len)(new Seq(['ccc', 'a', 'bb']))); $this->assertMap(['y' => 'a', 'x' => 'ccc'], c\sortBy($len)(new Map(['x' => 'ccc', 'y' => 'a']))); $this->assertSeq(['a', 'bb', 'ccc'], c\sortBy($len)(self::gen(['ccc', 'a', 'bb']))); } public function testSortByPassesKey() { $this->assertSame(['b' => 1, 'a' => 2], c\sortBy(fn ($v, $k) => $k, true)(['a' => 2, 'b' => 1])); } public function testSortDoesNotMutateInput() { $seq = new Seq([3, 1, 2]); $map = new Map(['a' => 3, 'b' => 1]); c\sortBy(fn ($v) => $v)($seq); c\sort()($seq); c\sort()($map); $this->assertSame([3, 1, 2], $seq->toArray()); $this->assertSame(['a' => 3, 'b' => 1], $map->toArray()); } // slice / take / skip public function testSlice() { $this->assertSame(['b' => 2, 'c' => 3], c\slice(1, 2)(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4])); $this->assertSeq([2, 3], c\slice(1, 2)(new Seq([1, 2, 3, 4]))); $this->assertMap(['b' => 2, 'c' => 3], c\slice(1, 2)(new Map(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]))); $this->assertSeq([2, 3], c\slice(1, 2)(self::gen([1, 2, 3, 4]))); } public function testSliceWithNegativeArguments() { $this->assertSame([2 => 3, 3 => 4], c\slice(-2)([1, 2, 3, 4])); $this->assertSame([1 => 2], c\slice(1, -2)([1, 2, 3, 4])); $this->assertSeq([3], c\slice(-2, -1)(self::gen([1, 2, 3, 4]))); } public function testTake() { $this->assertSame(['a' => 1], c\take(1)(['a' => 1, 'b' => 2])); $this->assertSeq([1, 2], c\take(2)(new Seq([1, 2, 3]))); $this->assertMap(['a' => 1], c\take(1)(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([], c\take(0)(new Seq([1, 2, 3]))); } public function testTakeStopsConsumingGenerator() { $infinite = (function () { for ($i = 1; ; $i++) { yield $i; } })(); $this->assertSeq([1, 2, 3], c\take(3)($infinite)); } public function testSkip() { $this->assertSame(['b' => 2], c\skip(1)(['a' => 1, 'b' => 2])); $this->assertSeq([3], c\skip(2)(new Seq([1, 2, 3]))); $this->assertMap(['b' => 2], c\skip(1)(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([2, 3], c\skip(1)(self::gen([1, 2, 3]))); } // prepend / append / concat / merge public function testPrepend() { $this->assertSame([0, 1, 1, 2], c\prepend(0, 1)(['a' => 1, 'b' => 2])); $this->assertSeq([0, 1, 2], c\prepend(0)(new Seq([1, 2]))); $this->assertSeq([0, 1, 2], c\prepend(0)(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([0, 1, 2], c\prepend(0)(self::gen([1, 2]))); } public function testAppend() { $this->assertSame([1, 2, 3, 4], c\append(3, 4)(['a' => 1, 'b' => 2])); $this->assertSeq([1, 2, 3], c\append(3)(new Seq([1, 2]))); $this->assertSeq([1, 2, 3], c\append(3)(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([1, 2, 3], c\append(3)(self::gen([1, 2]))); } public function testConcat() { $this->assertSame([1, 2, 3, 4], c\concat(['x' => 3, 'a' => 4])(['a' => 1, 'b' => 2])); $this->assertSeq([1, 2, 3], c\concat(self::gen([3]))(new Seq([1, 2]))); $this->assertSeq([1, 2, 3], c\concat([3])(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([1, 2, 3], c\concat(new Seq([3]))(self::gen([1, 2]))); } public function testListOperationsOnSetGiveSet() { $result = c\append(2, 3)(new Set([1, 2])); $this->assertInstanceOf(Set::class, $result); $this->assertSame([1, 2, 3], $result->toArray()); } public function testMerge() { $this->assertSame(['a' => 1, 'b' => 3, 'c' => 4], c\merge(['b' => 3, 'c' => 4])(['a' => 1, 'b' => 2])); $this->assertSame([0 => 'x', 1 => 'b', 5 => 'c'], c\merge([0 => 'x', 5 => 'c'])(['a', 'b'])); $this->assertSeq([1, 2, 3], c\merge([3])(new Seq([1, 2]))); $this->assertMap(['a' => 1, 'b' => 3, 'c' => 4], c\merge(new Map(['b' => 3, 'c' => 4]))(new Map(['a' => 1, 'b' => 2]))); $this->assertSeq([1, 2, 3], c\merge([3])(self::gen([1, 2]))); } // each / tap public function testEachDoesNotReplaceValues() { $calls = 0; $seq = new Seq([1, 2, 3]); $result = c\each(function ($v) use (&$calls) { $calls++; return 'x'; })($seq); $this->assertSame([1, 2, 3], $result->toArray()); $this->assertSame([1, 2, 3], $seq->toArray()); $this->assertSame(3, $calls); } public function testEachPassesValueThenKey() { $seen = []; $fn = function ($v, $k) use (&$seen) { $seen[] = "$k:$v"; }; $this->assertSame(['a' => 1], c\each($fn)(['a' => 1])); $map = new Map(['b' => 2]); $this->assertSame($map, c\each($fn)($map)); $this->assertSeq([3], c\each($fn)(self::gen(['c' => 3]))); $this->assertSame(['a:1', 'b:2', 'c:3'], $seen); } public function testTapIsEach() { $seen = []; $this->assertSame([1, 2], c\tap(function ($v) use (&$seen) { $seen[] = $v; })([1, 2])); $this->assertSame([1, 2], $seen); } // object keys (Review Focus 3) public function testMapWithObjectKeysKeepsKeys() { $k1 = new \stdClass(); $k2 = new \stdClass(); $map = new Map(); $map->put($k1, 2); $map->put($k2, 1); foreach ([ c\map(fn ($v) => $v * 10), c\filter(), c\sortBy(fn ($v) => $v), c\uniqueBy(fn ($v, $k) => $k), ] as $transform) { $result = $transform($map); $this->assertInstanceOf(Map::class, $result); $this->assertTrue($result->hasKey($k1)); $this->assertTrue($result->hasKey($k2)); } } }