assertEquals($items, iterator_to_array($collection)); } /** * Provider for average tests * * @return array */ public static function avgProvider(): array { $items = [1, 2, 3]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests the avg method */ #[DataProvider('avgProvider')] public function testAvg(iterable $items): void { $collection = new Collection($items); $this->assertSame(2, $collection->avg()); $items = [['foo' => 1], ['foo' => 2], ['foo' => 3]]; $collection = new Collection($items); $this->assertSame(2, $collection->avg('foo')); } /** * Tests the avg method when on an empty collection */ public function testAvgWithEmptyCollection(): void { $collection = new Collection([]); $this->assertNull($collection->avg()); $collection = new Collection([null, null]); $this->assertSame(0, $collection->avg()); } /** * Provider for average tests with use of a matcher * * @return array */ public static function avgWithMatcherProvider(): array { $items = [['foo' => 1], ['foo' => 2], ['foo' => 3]]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * ests the avg method */ #[DataProvider('avgWithMatcherProvider')] public function testAvgWithMatcher(iterable $items): void { $collection = new Collection($items); $this->assertSame(2, $collection->avg('foo')); } /** * Provider for some median tests * * @return array */ public static function medianProvider(): array { $items = [5, 2, 4]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests the median method */ #[DataProvider('medianProvider')] public function testMedian(iterable $items): void { $collection = new Collection($items); $this->assertSame(4, $collection->median()); } /** * Tests the median method when on an empty collection */ public function testMedianWithEmptyCollection(): void { $collection = new Collection([]); $this->assertNull($collection->median()); $collection = new Collection([null, null]); $this->assertSame(0, $collection->median()); } /** * Tests the median method */ #[DataProvider('simpleProvider')] public function testMedianEven(iterable $items): void { $collection = new Collection($items); $this->assertSame(2.5, $collection->median()); } /** * Provider for median tests with use of a matcher * * @return array */ public static function medianWithMatcherProvider(): array { $items = [ ['invoice' => ['total' => 400]], ['invoice' => ['total' => 500]], ['invoice' => ['total' => 200]], ['invoice' => ['total' => 100]], ['invoice' => ['total' => 333]], ]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests the median method */ #[DataProvider('medianWithMatcherProvider')] public function testMedianWithMatcher(iterable $items): void { $this->assertSame(333, (new Collection($items))->median('invoice.total')); } /** * Tests that it is possible to convert an iterator into a collection */ public function testIteratorIsWrapped(): void { $items = new ArrayObject([1, 2, 3]); $collection = new Collection($items); $this->assertEquals(iterator_to_array($items), iterator_to_array($collection)); } /** * Test running a method over all elements in the collection */ public function testEach(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $results = []; $collection->each(function ($value, $key) use (&$results): void { $results[] = [$key => $value]; }); $this->assertSame([['a' => 1], ['b' => 2], ['c' => 3]], $results); } public static function filterProvider(): array { $items = [1, 2, 0, 3, false, 4, null, 5, '']; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Test filter() with no callback. */ #[DataProvider('filterProvider')] public function testFilterNoCallback(iterable $items): void { $collection = new Collection($items); $result = $collection->filter()->toArray(); $expected = [1, 2, 3, 4, 5]; $this->assertSame($expected, array_values($result)); } /** * Tests that it is possible to chain filter() as it returns a collection object */ public function testFilterChaining(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $filtered = $collection->filter(function ($value, $key, $iterator) { return $value > 2; }); $this->assertInstanceOf(Collection::class, $filtered); $results = []; $filtered->each(function ($value, $key) use (&$results): void { $results[] = [$key => $value]; }); $this->assertSame([['c' => 3]], $results); } /** * Tests reject */ public function testReject(): void { $collection = new Collection([]); $result = $collection->reject(function ($v) { return false; }); $this->assertSame([], iterator_to_array($result)); $this->assertInstanceOf(Collection::class, $result); $collection = new Collection(['a' => null, 'b' => 2, 'c' => false]); $result = $collection->reject(); $this->assertEquals(['a' => null, 'c' => false], iterator_to_array($result)); $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $result = $collection->reject(function ($v, $k, $items) use ($collection) { $this->assertSame($collection->getInnerIterator(), $items); return $v > 2; }); $this->assertEquals(['a' => 1, 'b' => 2], iterator_to_array($result)); } public function testUnique(): void { $collection = new Collection([]); $result = $collection->unique(); $this->assertSame([], iterator_to_array($result)); $this->assertInstanceOf(Collection::class, $result); $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $result = $collection->unique(); $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3], iterator_to_array($result)); $items = ['a' => 1, 'b' => 2, 'c' => 1, 'd' => 2, 'e' => 1, 'f' => 3]; $collection = new Collection($items); $result = $collection->unique(); $this->assertEquals(['a' => 1, 'b' => 2, 'f' => 3], iterator_to_array($result)); $result = $collection->unique(fn ($v) => (string)$v); $this->assertEquals(['a' => 1, 'b' => 2, 'f' => 3], iterator_to_array($result)); $result = $collection->unique(fn ($v, $k) => $k); $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 1, 'd' => 2, 'e' => 1, 'f' => 3], iterator_to_array($result)); } /** * Tests every when the callback returns true for all elements */ public function testEveryReturnTrue(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $results = []; $this->assertTrue($collection->every(function ($value, $key) use (&$results) { $results[] = [$key => $value]; return true; })); $this->assertSame([['a' => 1], ['b' => 2], ['c' => 3]], $results); } /** * Tests every when the callback returns false for one of the elements */ public function testEveryReturnFalse(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $results = []; $this->assertFalse($collection->every(function ($value, $key) use (&$results) { $results[] = [$key => $value]; return $key !== 'b'; })); $this->assertSame([['a' => 1], ['b' => 2]], $results); } /** * Tests some() when one of the calls return true */ public function testSomeReturnTrue(): void { $collection = new Collection([]); $result = $collection->some(function ($v) { return true; }); $this->assertFalse($result); $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $results = []; $this->assertTrue($collection->some(function ($value, $key) use (&$results) { $results[] = [$key => $value]; return $key === 'b'; })); $this->assertSame([['a' => 1], ['b' => 2]], $results); } /** * Tests some() when none of the calls return true */ public function testSomeReturnFalse(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $results = []; $this->assertFalse($collection->some(function ($value, $key) use (&$results) { $results[] = [$key => $value]; return false; })); $this->assertSame([['a' => 1], ['b' => 2], ['c' => 3]], $results); } /** * Tests contains */ public function testContains(): void { $collection = new Collection([]); $this->assertFalse($collection->contains('a')); $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $this->assertTrue($collection->contains(2)); $this->assertTrue($collection->contains(1)); $this->assertFalse($collection->contains(10)); $this->assertFalse($collection->contains('2')); } /** * Provider for some simple tests * * @return array */ public static function simpleProvider(): array { $items = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests map */ #[DataProvider('simpleProvider')] public function testMap(iterable $items): void { $collection = new Collection($items); $map = $collection->map(function ($v, $k, $it) use ($collection) { $this->assertSame($collection->getInnerIterator(), $it); return $v * $v; }); $this->assertInstanceOf(ReplaceIterator::class, $map); $this->assertEquals(['a' => 1, 'b' => 4, 'c' => 9, 'd' => 16], iterator_to_array($map)); } /** * Tests reduce with initial value */ #[DataProvider('simpleProvider')] public function testReduceWithInitialValue(iterable $items): void { $collection = new Collection($items); $this->assertSame(20, $collection->reduce(function ($reduction, $value, $key) { return $value + $reduction; }, 10)); } /** * Tests reduce without initial value */ #[DataProvider('simpleProvider')] public function testReduceWithoutInitialValue(iterable $items): void { $collection = new Collection($items); $this->assertSame(10, $collection->reduce(function ($reduction, $value, $key) { return $value + $reduction; })); } /** * Provider for some extract tests * * @return array */ public static function extractProvider(): array { $items = [['a' => ['b' => ['c' => 1]]], 2]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests extract */ #[DataProvider('extractProvider')] public function testExtract(iterable $items): void { $collection = new Collection($items); $map = $collection->extract('a.b.c'); $this->assertInstanceOf(ExtractIterator::class, $map); $this->assertEquals([1, null], iterator_to_array($map)); } /** * Provider for some sort tests * * @return array */ public static function sortProvider(): array { $items = [ ['a' => ['b' => ['c' => 4]]], ['a' => ['b' => ['c' => 10]]], ['a' => ['b' => ['c' => 6]]], ]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests sort */ #[DataProvider('sortProvider')] public function testSortString(iterable $items): void { $collection = new Collection($items); $map = $collection->sortBy('a.b.c'); $this->assertInstanceOf(Collection::class, $map); $expected = [ ['a' => ['b' => ['c' => 10]]], ['a' => ['b' => ['c' => 6]]], ['a' => ['b' => ['c' => 4]]], ]; $this->assertEquals($expected, $map->toList()); } /** * Tests max */ #[DataProvider('sortProvider')] public function testMax(iterable $items): void { $collection = new Collection($items); $this->assertEquals(['a' => ['b' => ['c' => 10]]], $collection->max('a.b.c')); } /** * Tests max */ #[DataProvider('sortProvider')] public function testMaxCallback(iterable $items): void { $collection = new Collection($items); $callback = function ($e) { return $e['a']['b']['c'] * -1; }; $this->assertEquals(['a' => ['b' => ['c' => 4]]], $collection->max($callback)); } /** * Tests max */ #[DataProvider('sortProvider')] public function testMaxCallable(iterable $items): void { $collection = new Collection($items); $this->assertEquals(['a' => ['b' => ['c' => 4]]], $collection->max(function ($e) { return $e['a']['b']['c'] * -1; })); } /** * Test max with a collection of Entities */ public function testMaxWithEntities(): void { $collection = new Collection([ new Entity(['id' => 1, 'count' => 18]), new Entity(['id' => 2, 'count' => 9]), new Entity(['id' => 3, 'count' => 42]), new Entity(['id' => 4, 'count' => 4]), new Entity(['id' => 5, 'count' => 22]), ]); $expected = new Entity(['id' => 3, 'count' => 42]); $this->assertEquals($expected, $collection->max('count')); } /** * Tests min */ #[DataProvider('sortProvider')] public function testMin(iterable $items): void { $collection = new Collection($items); $this->assertEquals(['a' => ['b' => ['c' => 4]]], $collection->min('a.b.c')); } /** * Test min with a collection of Entities */ public function testMinWithEntities(): void { $collection = new Collection([ new Entity(['id' => 1, 'count' => 18]), new Entity(['id' => 2, 'count' => 9]), new Entity(['id' => 3, 'count' => 42]), new Entity(['id' => 4, 'count' => 4]), new Entity(['id' => 5, 'count' => 22]), ]); $expected = new Entity(['id' => 4, 'count' => 4]); $this->assertEquals($expected, $collection->min('count')); } /** * Provider for some groupBy tests * * @return array */ public static function groupByProvider(): array { $items = [ ['id' => 1, 'name' => 'foo', 'parent_id' => 10], ['id' => 2, 'name' => 'bar', 'parent_id' => 11], ['id' => 3, 'name' => 'baz', 'parent_id' => 10], ]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests groupBy */ #[DataProvider('groupByProvider')] public function testGroupBy(iterable $items): void { $collection = new Collection($items); $grouped = $collection->groupBy('parent_id'); $expected = [ 10 => [ ['id' => 1, 'name' => 'foo', 'parent_id' => 10], ['id' => 3, 'name' => 'baz', 'parent_id' => 10], ], 11 => [ ['id' => 2, 'name' => 'bar', 'parent_id' => 11], ], ]; $this->assertEquals($expected, iterator_to_array($grouped)); $this->assertInstanceOf(Collection::class, $grouped); } /** * Tests groupBy */ #[DataProvider('groupByProvider')] public function testGroupByCallback(iterable $items): void { $collection = new Collection($items); $expected = [ 10 => [ ['id' => 1, 'name' => 'foo', 'parent_id' => 10], ['id' => 3, 'name' => 'baz', 'parent_id' => 10], ], 11 => [ ['id' => 2, 'name' => 'bar', 'parent_id' => 11], ], ]; $grouped = $collection->groupBy(function ($element) { return $element['parent_id']; }); $this->assertEquals($expected, iterator_to_array($grouped)); } public function testGroupByPreserveIndex(): void { $items = [ 'first' => ['name' => 'foo', 'type' => 'a'], 'second' => ['name' => 'bar', 'type' => 'b'], 'third' => ['name' => 'baz', 'type' => 'b'], 'fourth' => ['name' => 'aah', 'type' => 'a'], ]; $collection = new Collection($items); $grouped = $collection->groupBy('type', true); $expected = [ 'a' => [ 'first' => ['name' => 'foo', 'type' => 'a'], 'fourth' => ['name' => 'aah', 'type' => 'a'], ], 'b' => [ 'second' => ['name' => 'bar', 'type' => 'b'], 'third' => ['name' => 'baz', 'type' => 'b'], ], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests grouping by a deep key */ public function testGroupByDeepKey(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]], ['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]], ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], ]; $collection = new Collection($items); $grouped = $collection->groupBy('thing.parent_id'); $expected = [ 10 => [ ['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]], ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], ], 11 => [ ['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]], ], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests grouping by an enum key */ public function testGroupByEnum(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => NonBacked::Basic], ['id' => 2, 'name' => 'bar', 'thing' => NonBacked::Advanced], ['id' => 3, 'name' => 'baz', 'thing' => NonBacked::Basic], ]; $collection = new Collection($items); $grouped = $collection->groupBy('thing'); $expected = [ NonBacked::Basic->name => [ ['id' => 1, 'name' => 'foo', 'thing' => NonBacked::Basic], ['id' => 3, 'name' => 'baz', 'thing' => NonBacked::Basic], ], NonBacked::Advanced->name => [ ['id' => 2, 'name' => 'bar', 'thing' => NonBacked::Advanced], ], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests grouping by a backed enum key */ public function testGroupByBackedEnum(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => Priority::Medium], ['id' => 2, 'name' => 'bar', 'thing' => Priority::High], ['id' => 3, 'name' => 'baz', 'thing' => Priority::Medium], ]; $collection = new Collection($items); $grouped = $collection->groupBy('thing'); $expected = [ Priority::Medium->value => [ ['id' => 1, 'name' => 'foo', 'thing' => Priority::Medium], ['id' => 3, 'name' => 'baz', 'thing' => Priority::Medium], ], Priority::High->value => [ ['id' => 2, 'name' => 'bar', 'thing' => Priority::High], ], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests passing an invalid path to groupBy. */ public function testGroupByInvalidPath(): void { $items = [ ['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar'], ['id' => 3, 'name' => 'baz'], ]; $collection = new Collection($items); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot group by path that does not exist or contains a null value.'); $collection->groupBy('missing'); } /** * Provider for some indexBy tests * * @return array */ public static function indexByProvider(): array { $items = [ ['id' => 1, 'name' => 'foo', 'parent_id' => 10], ['id' => 2, 'name' => 'bar', 'parent_id' => 11], ['id' => 3, 'name' => 'baz', 'parent_id' => 10], ]; return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests indexBy */ #[DataProvider('indexByProvider')] public function testIndexBy(iterable $items): void { $collection = new Collection($items); $grouped = $collection->indexBy('id'); $expected = [ 1 => ['id' => 1, 'name' => 'foo', 'parent_id' => 10], 3 => ['id' => 3, 'name' => 'baz', 'parent_id' => 10], 2 => ['id' => 2, 'name' => 'bar', 'parent_id' => 11], ]; $this->assertEquals($expected, iterator_to_array($grouped)); $this->assertInstanceOf(Collection::class, $grouped); } /** * Tests indexBy */ #[DataProvider('indexByProvider')] public function testIndexByCallback(iterable $items): void { $collection = new Collection($items); $grouped = $collection->indexBy(function ($element) { return $element['id']; }); $expected = [ 1 => ['id' => 1, 'name' => 'foo', 'parent_id' => 10], 3 => ['id' => 3, 'name' => 'baz', 'parent_id' => 10], 2 => ['id' => 2, 'name' => 'bar', 'parent_id' => 11], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests indexBy with an enum */ public function testIndexByEnum(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => NonBacked::Basic], ['id' => 2, 'name' => 'bar', 'thing' => NonBacked::Advanced], ]; $collection = new Collection($items); $grouped = $collection->indexBy(function ($element) { return $element['thing']; }); $expected = [ NonBacked::Basic->name => ['id' => 1, 'name' => 'foo', 'thing' => NonBacked::Basic], NonBacked::Advanced->name => ['id' => 2, 'name' => 'bar', 'thing' => NonBacked::Advanced], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests indexBy with a backed enum */ public function testIndexByBackedEnum(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => Priority::Medium], ['id' => 2, 'name' => 'bar', 'thing' => Priority::High], ]; $collection = new Collection($items); $grouped = $collection->indexBy(function ($element) { return $element['thing']; }); $expected = [ Priority::Medium->value => ['id' => 1, 'name' => 'foo', 'thing' => Priority::Medium], Priority::High->value => ['id' => 2, 'name' => 'bar', 'thing' => Priority::High], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests indexBy with a deep property */ public function testIndexByDeep(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]], ['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]], ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], ]; $collection = new Collection($items); $grouped = $collection->indexBy('thing.parent_id'); $expected = [ 10 => ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], 11 => ['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]], ]; $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests passing an invalid path to indexBy. */ public function testIndexByInvalidPath(): void { $items = [ ['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar'], ['id' => 3, 'name' => 'baz'], ]; $collection = new Collection($items); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot index by path that does not exist or contains a null value'); $collection->indexBy('missing'); } /** * Tests passing an invalid path to indexBy. */ public function testIndexByInvalidPathCallback(): void { $items = [ ['id' => 1, 'name' => 'foo'], ['id' => 2, 'name' => 'bar'], ['id' => 3, 'name' => 'baz'], ]; $collection = new Collection($items); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot index by path that does not exist or contains a null value'); $collection->indexBy(function ($e) { return null; }); } /** * Tests countBy */ #[DataProvider('groupByProvider')] public function testCountBy(iterable $items): void { $collection = new Collection($items); $grouped = $collection->countBy('parent_id'); $expected = [ 10 => 2, 11 => 1, ]; $result = iterator_to_array($grouped); $this->assertInstanceOf(Collection::class, $grouped); $this->assertEquals($expected, $result); } /** * Tests countBy */ #[DataProvider('groupByProvider')] public function testCountByCallback(iterable $items): void { $expected = [ 10 => 2, 11 => 1, ]; $collection = new Collection($items); $grouped = $collection->countBy(function ($element) { return $element['parent_id']; }); $this->assertEquals($expected, iterator_to_array($grouped)); } /** * Tests shuffle */ #[DataProvider('simpleProvider')] public function testShuffle(iterable $data): void { $collection = (new Collection($data))->shuffle(); $result = $collection->toArray(); $this->assertCount(4, $result); $this->assertContains(1, $result); $this->assertContains(2, $result); $this->assertContains(3, $result); $this->assertContains(4, $result); } /** * Tests shuffle with duplicate keys. */ public function testShuffleDuplicateKeys(): void { $collection = (new Collection(['a' => 1]))->append(['a' => 2])->shuffle(); $result = $collection->toArray(); $this->assertCount(2, $result); $this->assertEquals([0, 1], array_keys($result)); $this->assertContainsEquals(1, $result); $this->assertContainsEquals(2, $result); } /** * Tests sample */ #[DataProvider('simpleProvider')] public function testSample(iterable $data): void { $result = (new Collection($data))->sample(2)->toArray(); $this->assertCount(2, $result); foreach ($result as $number) { $this->assertContains($number, [1, 2, 3, 4]); } } /** * Tests the sample() method with a traversable non-iterator */ public function testSampleWithTraversableNonIterator(): void { $collection = new Collection($this->datePeriod('2017-01-01', '2017-01-07')); $result = $collection->sample(3)->toList(); $list = [ '2017-01-01', '2017-01-02', '2017-01-03', '2017-01-04', '2017-01-05', '2017-01-06', ]; $this->assertCount(3, $result); foreach ($result as $date) { $this->assertContains($date->format('Y-m-d'), $list); } } /** * Test toArray method */ public function testToArray(): void { $data = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; $collection = new Collection($data); $this->assertEquals($data, $collection->toArray()); } /** * Test toList method */ #[DataProvider('simpleProvider')] public function testToList(iterable $data): void { $collection = new Collection($data); $this->assertEquals([1, 2, 3, 4], $collection->toList()); } /** * Test JSON encoding */ public function testToJson(): void { $data = [1, 2, 3, 4]; $collection = new Collection($data); $this->assertEquals(json_encode($data), json_encode($collection)); } /** * Tests that Count returns the number of elements */ #[DataProvider('simpleProvider')] public function testCollectionCount(iterable $list): void { $list = (new Collection($list))->buffered(); $collection = new Collection($list); $this->assertSame(8, $collection->append($list)->count()); } /** * Tests that countKeys returns the number of unique keys */ #[DataProvider('simpleProvider')] public function testCollectionCountKeys(iterable $list): void { $list = (new Collection($list))->buffered(); $collection = new Collection($list); $this->assertSame(4, $collection->append($list)->countKeys()); } /** * Tests take method */ public function testTake(): void { $data = [1, 2, 3, 4]; $collection = new Collection($data); $taken = $collection->take(2); $this->assertEquals([1, 2], $taken->toArray()); $taken = $collection->take(3); $this->assertEquals([1, 2, 3], $taken->toArray()); $taken = $collection->take(500); $this->assertEquals([1, 2, 3, 4], $taken->toArray()); $taken = $collection->take(1); $this->assertEquals([1], $taken->toArray()); $taken = $collection->take(); $this->assertEquals([1], $taken->toArray()); $taken = $collection->take(2, 2); $this->assertEquals([2 => 3, 3 => 4], $taken->toArray()); } /** * Tests the take() method with a traversable non-iterator */ public function testTakeWithTraversableNonIterator(): void { $collection = new Collection($this->datePeriod('2017-01-01', '2017-01-07')); $result = $collection->take(3, 1)->toList(); $expected = [ new DateTime('2017-01-02'), new DateTime('2017-01-03'), new DateTime('2017-01-04'), ]; $this->assertEquals($expected, $result); } /** * Tests match */ public function testMatch(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]], ['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]], ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], ]; $collection = new Collection($items); $matched = $collection->match(['thing.parent_id' => 10, 'name' => 'baz']); $this->assertEquals([2 => $items[2]], $matched->toArray()); $matched = $collection->match(['thing.parent_id' => 10]); $this->assertEquals( [0 => $items[0], 2 => $items[2]], $matched->toArray() ); $matched = $collection->match(['thing.parent_id' => 500]); $this->assertEquals([], $matched->toArray()); $matched = $collection->match(['parent_id' => 10, 'name' => 'baz']); $this->assertEquals([], $matched->toArray()); } /** * Tests firstMatch */ public function testFirstMatch(): void { $items = [ ['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]], ['id' => 2, 'name' => 'bar', 'thing' => ['parent_id' => 11]], ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], ]; $collection = new Collection($items); $matched = $collection->firstMatch(['thing.parent_id' => 10]); $this->assertEquals( ['id' => 1, 'name' => 'foo', 'thing' => ['parent_id' => 10]], $matched ); $matched = $collection->firstMatch(['thing.parent_id' => 10, 'name' => 'baz']); $this->assertEquals( ['id' => 3, 'name' => 'baz', 'thing' => ['parent_id' => 10]], $matched ); } /** * Tests the append method */ public function testAppend(): void { $collection = new Collection([1, 2, 3]); $combined = $collection->append([4, 5, 6]); $this->assertEquals([1, 2, 3, 4, 5, 6], $combined->toArray(false)); $collection = new Collection(['a' => 1, 'b' => 2]); $combined = $collection->append(['c' => 3, 'a' => 4]); $this->assertEquals(['a' => 4, 'b' => 2, 'c' => 3], $combined->toArray()); } /** * Tests the appendItem method */ public function testAppendItem(): void { $collection = new Collection([1, 2, 3]); $combined = $collection->appendItem(4); $this->assertEquals([1, 2, 3, 4], $combined->toArray(false)); $collection = new Collection(['a' => 1, 'b' => 2]); $combined = $collection->appendItem(3, 'c'); $combined = $combined->appendItem(4, 'a'); $this->assertEquals(['a' => 4, 'b' => 2, 'c' => 3], $combined->toArray()); } /** * Tests the prepend method */ public function testPrepend(): void { $collection = new Collection([1, 2, 3]); $combined = $collection->prepend(['a']); $this->assertEquals(['a', 1, 2, 3], $combined->toList()); $collection = new Collection(['c' => 3, 'd' => 4]); $combined = $collection->prepend(['a' => 1, 'b' => 2]); $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $combined->toArray()); } /** * Tests prependItem method */ public function testPrependItem(): void { $collection = new Collection([1, 2, 3]); $combined = $collection->prependItem('a'); $this->assertEquals(['a', 1, 2, 3], $combined->toList()); $collection = new Collection(['c' => 3, 'd' => 4]); $combined = $collection->prependItem(2, 'b'); $combined = $combined->prependItem(1, 'a'); $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $combined->toArray()); } /** * Tests prependItem method */ public function testPrependItemPreserveKeys(): void { $collection = new Collection([1, 2, 3]); $combined = $collection->prependItem('a'); $this->assertEquals(['a', 1, 2, 3], $combined->toList()); $collection = new Collection(['c' => 3, 'd' => 4]); $combined = $collection->prependItem(2, 'b'); $combined = $combined->prependItem(1, 'a'); $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4], $combined->toArray()); } /** * Tests the append method with iterator */ public function testAppendIterator(): void { $collection = new Collection([1, 2, 3]); $iterator = new ArrayIterator([4, 5, 6]); $combined = $collection->append($iterator); $this->assertEquals([1, 2, 3, 4, 5, 6], $combined->toList()); } public function testAppendNotCollectionInstance(): void { $collection = new TestCollection([1, 2, 3]); $combined = $collection->append([4, 5, 6]); $this->assertEquals([1, 2, 3, 4, 5, 6], $combined->toList()); } /** * Tests that by calling compile internal iteration operations are not done * more than once */ public function testCompile(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = new Collection($items); $results = []; $compiled = $collection ->map(function ($value, $key) use (&$results) { $results[] = [$key => $value]; return $value + 3; }) ->compile(); $this->assertSame(['a' => 4, 'b' => 5, 'c' => 6], $compiled->toArray()); $this->assertSame(['a' => 4, 'b' => 5, 'c' => 6], $compiled->toArray()); $this->assertSame([['a' => 1], ['b' => 2], ['c' => 3]], $results); } /** * Tests converting a non rewindable iterator into a rewindable one using * the buffered method. */ public function testBuffered(): void { $items = new NoRewindIterator(new ArrayIterator(['a' => 4, 'b' => 5, 'c' => 6])); $buffered = (new Collection($items))->buffered(); $this->assertEquals(['a' => 4, 'b' => 5, 'c' => 6], $buffered->toArray()); $this->assertEquals(['a' => 4, 'b' => 5, 'c' => 6], $buffered->toArray()); } public function testBufferedIterator(): void { $data = [ ['myField' => '1'], ['myField' => '2'], ['myField' => '3'], ]; $buffered = (new Collection($data))->buffered(); // Check going forwards $this->assertNotEmpty($buffered->firstMatch(['myField' => '1'])); $this->assertNotEmpty($buffered->firstMatch(['myField' => '2'])); $this->assertNotEmpty($buffered->firstMatch(['myField' => '3'])); // And backwards. $this->assertNotEmpty($buffered->firstMatch(['myField' => '3'])); $this->assertNotEmpty($buffered->firstMatch(['myField' => '2'])); $this->assertNotEmpty($buffered->firstMatch(['myField' => '1'])); } /** * Tests the combine method */ public function testCombine(): void { $items = [ ['id' => 1, 'name' => 'foo', 'parent' => 'a'], ['id' => 2, 'name' => 'bar', 'parent' => 'b'], ['id' => 3, 'name' => 'baz', 'parent' => 'a'], ]; $collection = (new Collection($items))->combine('id', 'name'); $expected = [1 => 'foo', 2 => 'bar', 3 => 'baz']; $this->assertEquals($expected, $collection->toArray()); $expected = ['foo' => 1, 'bar' => 2, 'baz' => 3]; $collection = (new Collection($items))->combine('name', 'id'); $this->assertEquals($expected, $collection->toArray()); $collection = (new Collection($items))->combine('id', 'name', 'parent'); $expected = ['a' => [1 => 'foo', 3 => 'baz'], 'b' => [2 => 'bar']]; $this->assertEquals($expected, $collection->toArray()); $expected = [ '0-1' => ['foo-0-1' => '0-1-foo'], '1-2' => ['bar-1-2' => '1-2-bar'], '2-3' => ['baz-2-3' => '2-3-baz'], ]; $collection = (new Collection($items))->combine( function ($value, $key) { return $value['name'] . '-' . $key; }, function ($value, $key) { return $key . '-' . $value['name']; }, function ($value, $key) { return $key . '-' . $value['id']; } ); $this->assertEquals($expected, $collection->toArray()); $collection = (new Collection($items))->combine('id', 'crazy'); $this->assertEquals([1 => null, 2 => null, 3 => null], $collection->toArray()); $collection = (new Collection([ ['amount' => 10, 'article_status' => ArticleStatus::from('Y')], ['amount' => 2, 'article_status' => ArticleStatus::from('N')], ]))->combine('article_status', 'amount'); $this->assertEquals(['Y' => 10, 'N' => 2], $collection->toArray()); } public function testCombineWithNonBackedEnum(): void { $collection = (new Collection([ ['amount' => 10, 'type' => NonBacked::Basic], ['amount' => 2, 'type' => NonBacked::Advanced], ]))->combine('type', 'amount'); $this->assertEquals(['Basic' => 10, 'Advanced' => 2], $collection->toArray()); } public function testCombineNullKey(): void { $items = [ ['id' => 1, 'name' => 'foo', 'parent' => 'a'], ['id' => null, 'name' => 'bar', 'parent' => 'b'], ['id' => 3, 'name' => 'baz', 'parent' => 'a'], ]; $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot index by path that does not exist or contains a null value'); (new Collection($items))->combine('id', 'name'); } public function testCombineNullGroup(): void { $items = [ ['id' => 1, 'name' => 'foo', 'parent' => 'a'], ['id' => 2, 'name' => 'bar', 'parent' => 'b'], ['id' => 3, 'name' => 'baz', 'parent' => null], ]; $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot group by path that does not exist or contains a null value'); (new Collection($items))->combine('id', 'name', 'parent'); } public function testCombineGroupNullKey(): void { $items = [ ['id' => 1, 'name' => 'foo', 'parent' => 'a'], ['id' => 2, 'name' => 'bar', 'parent' => 'b'], ['id' => null, 'name' => 'baz', 'parent' => 'a'], ]; $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('Cannot index by path that does not exist or contains a null value'); (new Collection($items))->combine('id', 'name', 'parent'); } /** * Tests the nest method with only one level */ public function testNest(): void { $items = [ ['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => 1], ['id' => 3, 'parent_id' => 1], ['id' => 4, 'parent_id' => 1], ['id' => 5, 'parent_id' => 6], ['id' => 6, 'parent_id' => null], ['id' => 7, 'parent_id' => 1], ['id' => 8, 'parent_id' => 6], ['id' => 9, 'parent_id' => 6], ['id' => 10, 'parent_id' => 6], ]; $collection = (new Collection($items))->nest('id', 'parent_id'); $expected = [ [ 'id' => 1, 'parent_id' => null, 'children' => [ ['id' => 2, 'parent_id' => 1, 'children' => []], ['id' => 3, 'parent_id' => 1, 'children' => []], ['id' => 4, 'parent_id' => 1, 'children' => []], ['id' => 7, 'parent_id' => 1, 'children' => []], ], ], [ 'id' => 6, 'parent_id' => null, 'children' => [ ['id' => 5, 'parent_id' => 6, 'children' => []], ['id' => 8, 'parent_id' => 6, 'children' => []], ['id' => 9, 'parent_id' => 6, 'children' => []], ['id' => 10, 'parent_id' => 6, 'children' => []], ], ], ]; $this->assertEquals($expected, $collection->toArray()); } /** * Tests the nest method with alternate nesting key */ public function testNestAlternateNestingKey(): void { $items = [ ['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => 1], ['id' => 3, 'parent_id' => 1], ['id' => 4, 'parent_id' => 1], ['id' => 5, 'parent_id' => 6], ['id' => 6, 'parent_id' => null], ['id' => 7, 'parent_id' => 1], ['id' => 8, 'parent_id' => 6], ['id' => 9, 'parent_id' => 6], ['id' => 10, 'parent_id' => 6], ]; $collection = (new Collection($items))->nest('id', 'parent_id', 'nodes'); $expected = [ [ 'id' => 1, 'parent_id' => null, 'nodes' => [ ['id' => 2, 'parent_id' => 1, 'nodes' => []], ['id' => 3, 'parent_id' => 1, 'nodes' => []], ['id' => 4, 'parent_id' => 1, 'nodes' => []], ['id' => 7, 'parent_id' => 1, 'nodes' => []], ], ], [ 'id' => 6, 'parent_id' => null, 'nodes' => [ ['id' => 5, 'parent_id' => 6, 'nodes' => []], ['id' => 8, 'parent_id' => 6, 'nodes' => []], ['id' => 9, 'parent_id' => 6, 'nodes' => []], ['id' => 10, 'parent_id' => 6, 'nodes' => []], ], ], ]; $this->assertEquals($expected, $collection->toArray()); } /** * Tests the nest method with more than one level */ public function testNestMultiLevel(): void { $items = [ ['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => 1], ['id' => 3, 'parent_id' => 2], ['id' => 4, 'parent_id' => 2], ['id' => 5, 'parent_id' => 3], ['id' => 6, 'parent_id' => null], ['id' => 7, 'parent_id' => 3], ['id' => 8, 'parent_id' => 4], ['id' => 9, 'parent_id' => 6], ['id' => 10, 'parent_id' => 6], ]; $collection = (new Collection($items))->nest('id', 'parent_id', 'nodes'); $expected = [ [ 'id' => 1, 'parent_id' => null, 'nodes' => [ [ 'id' => 2, 'parent_id' => 1, 'nodes' => [ [ 'id' => 3, 'parent_id' => 2, 'nodes' => [ ['id' => 5, 'parent_id' => 3, 'nodes' => []], ['id' => 7, 'parent_id' => 3, 'nodes' => []], ], ], [ 'id' => 4, 'parent_id' => 2, 'nodes' => [ ['id' => 8, 'parent_id' => 4, 'nodes' => []], ], ], ], ], ], ], [ 'id' => 6, 'parent_id' => null, 'nodes' => [ ['id' => 9, 'parent_id' => 6, 'nodes' => []], ['id' => 10, 'parent_id' => 6, 'nodes' => []], ], ], ]; $this->assertEquals($expected, $collection->toArray()); } /** * Tests the nest method with more than one level */ public function testNestMultiLevelAlternateNestingKey(): void { $items = [ ['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => 1], ['id' => 3, 'parent_id' => 2], ['id' => 4, 'parent_id' => 2], ['id' => 5, 'parent_id' => 3], ['id' => 6, 'parent_id' => null], ['id' => 7, 'parent_id' => 3], ['id' => 8, 'parent_id' => 4], ['id' => 9, 'parent_id' => 6], ['id' => 10, 'parent_id' => 6], ]; $collection = (new Collection($items))->nest('id', 'parent_id'); $expected = [ [ 'id' => 1, 'parent_id' => null, 'children' => [ [ 'id' => 2, 'parent_id' => 1, 'children' => [ [ 'id' => 3, 'parent_id' => 2, 'children' => [ ['id' => 5, 'parent_id' => 3, 'children' => []], ['id' => 7, 'parent_id' => 3, 'children' => []], ], ], [ 'id' => 4, 'parent_id' => 2, 'children' => [ ['id' => 8, 'parent_id' => 4, 'children' => []], ], ], ], ], ], ], [ 'id' => 6, 'parent_id' => null, 'children' => [ ['id' => 9, 'parent_id' => 6, 'children' => []], ['id' => 10, 'parent_id' => 6, 'children' => []], ], ], ]; $this->assertEquals($expected, $collection->toArray()); } /** * Tests the nest method with more than one level */ public function testNestObjects(): void { $items = [ new ArrayObject(['id' => 1, 'parent_id' => null]), new ArrayObject(['id' => 2, 'parent_id' => 1]), new ArrayObject(['id' => 3, 'parent_id' => 2]), new ArrayObject(['id' => 4, 'parent_id' => 2]), new ArrayObject(['id' => 5, 'parent_id' => 3]), new ArrayObject(['id' => 6, 'parent_id' => null]), new ArrayObject(['id' => 7, 'parent_id' => 3]), new ArrayObject(['id' => 8, 'parent_id' => 4]), new ArrayObject(['id' => 9, 'parent_id' => 6]), new ArrayObject(['id' => 10, 'parent_id' => 6]), ]; $collection = (new Collection($items))->nest('id', 'parent_id'); $expected = [ new ArrayObject([ 'id' => 1, 'parent_id' => null, 'children' => [ new ArrayObject([ 'id' => 2, 'parent_id' => 1, 'children' => [ new ArrayObject([ 'id' => 3, 'parent_id' => 2, 'children' => [ new ArrayObject(['id' => 5, 'parent_id' => 3, 'children' => []]), new ArrayObject(['id' => 7, 'parent_id' => 3, 'children' => []]), ], ]), new ArrayObject([ 'id' => 4, 'parent_id' => 2, 'children' => [ new ArrayObject(['id' => 8, 'parent_id' => 4, 'children' => []]), ], ]), ], ]), ], ]), new ArrayObject([ 'id' => 6, 'parent_id' => null, 'children' => [ new ArrayObject(['id' => 9, 'parent_id' => 6, 'children' => []]), new ArrayObject(['id' => 10, 'parent_id' => 6, 'children' => []]), ], ]), ]; $this->assertEquals($expected, $collection->toArray()); } /** * Tests the nest method with more than one level */ public function testNestObjectsAlternateNestingKey(): void { $items = [ new ArrayObject(['id' => 1, 'parent_id' => null]), new ArrayObject(['id' => 2, 'parent_id' => 1]), new ArrayObject(['id' => 3, 'parent_id' => 2]), new ArrayObject(['id' => 4, 'parent_id' => 2]), new ArrayObject(['id' => 5, 'parent_id' => 3]), new ArrayObject(['id' => 6, 'parent_id' => null]), new ArrayObject(['id' => 7, 'parent_id' => 3]), new ArrayObject(['id' => 8, 'parent_id' => 4]), new ArrayObject(['id' => 9, 'parent_id' => 6]), new ArrayObject(['id' => 10, 'parent_id' => 6]), ]; $collection = (new Collection($items))->nest('id', 'parent_id', 'nodes'); $expected = [ new ArrayObject([ 'id' => 1, 'parent_id' => null, 'nodes' => [ new ArrayObject([ 'id' => 2, 'parent_id' => 1, 'nodes' => [ new ArrayObject([ 'id' => 3, 'parent_id' => 2, 'nodes' => [ new ArrayObject(['id' => 5, 'parent_id' => 3, 'nodes' => []]), new ArrayObject(['id' => 7, 'parent_id' => 3, 'nodes' => []]), ], ]), new ArrayObject([ 'id' => 4, 'parent_id' => 2, 'nodes' => [ new ArrayObject(['id' => 8, 'parent_id' => 4, 'nodes' => []]), ], ]), ], ]), ], ]), new ArrayObject([ 'id' => 6, 'parent_id' => null, 'nodes' => [ new ArrayObject(['id' => 9, 'parent_id' => 6, 'nodes' => []]), new ArrayObject(['id' => 10, 'parent_id' => 6, 'nodes' => []]), ], ]), ]; $this->assertEquals($expected, $collection->toArray()); } /** * Tests insert */ public function testInsert(): void { $items = [['a' => 1], ['b' => 2]]; $collection = new Collection($items); $iterator = $collection->insert('c', [3, 4]); $this->assertInstanceOf(InsertIterator::class, $iterator); $this->assertEquals( [['a' => 1, 'c' => 3], ['b' => 2, 'c' => 4]], iterator_to_array($iterator) ); } /** * Provider for testing each of the directions for listNested * * @return array */ public static function nestedListProvider(): array { return [ ['desc', [1, 2, 3, 5, 7, 4, 8, 6, 9, 10]], ['asc', [5, 7, 3, 8, 4, 2, 1, 9, 10, 6]], ['leaves', [5, 7, 8, 9, 10]], ]; } /** * Tests the listNested method with the default 'children' nesting key */ #[DataProvider('nestedListProvider')] public function testListNested(string $dir, array $expected): void { $items = [ ['id' => 1, 'parent_id' => null], ['id' => 2, 'parent_id' => 1], ['id' => 3, 'parent_id' => 2], ['id' => 4, 'parent_id' => 2], ['id' => 5, 'parent_id' => 3], ['id' => 6, 'parent_id' => null], ['id' => 7, 'parent_id' => 3], ['id' => 8, 'parent_id' => 4], ['id' => 9, 'parent_id' => 6], ['id' => 10, 'parent_id' => 6], ]; $collection = (new Collection($items))->nest('id', 'parent_id')->listNested($dir); $this->assertEquals($expected, $collection->extract('id')->toArray(false)); } /** * Tests the listNested spacer output. */ public function testListNestedSpacer(): void { $items = [ ['id' => 1, 'parent_id' => null, 'name' => 'Birds'], ['id' => 2, 'parent_id' => 1, 'name' => 'Land Birds'], ['id' => 3, 'parent_id' => 1, 'name' => 'Eagle'], ['id' => 4, 'parent_id' => 1, 'name' => 'Seagull'], ['id' => 5, 'parent_id' => 6, 'name' => 'Clown Fish'], ['id' => 6, 'parent_id' => null, 'name' => 'Fish'], ]; $collection = (new Collection($items))->nest('id', 'parent_id')->listNested(); $expected = [ 'Birds', '---Land Birds', '---Eagle', '---Seagull', 'Fish', '---Clown Fish', ]; $this->assertSame($expected, $collection->printer('name', 'id', '---')->toList()); } /** * Tests using listNested with a different nesting key */ public function testListNestedCustomKey(): void { $items = [ ['id' => 1, 'stuff' => [['id' => 2, 'stuff' => [['id' => 3]]]]], ['id' => 4, 'stuff' => [['id' => 5]]], ]; $collection = (new Collection($items))->listNested('desc', 'stuff'); $this->assertEquals(range(1, 5), $collection->extract('id')->toArray(false)); } /** * Tests flattening the collection using a custom callable function */ public function testListNestedWithCallable(): void { $items = [ ['id' => 1, 'stuff' => [['id' => 2, 'stuff' => [['id' => 3]]]]], ['id' => 4, 'stuff' => [['id' => 5]]], ]; $collection = (new Collection($items))->listNested('desc', function ($item) { return $item['stuff'] ?? []; }); $this->assertEquals(range(1, 5), $collection->extract('id')->toArray(false)); } /** * Provider for sumOf tests * * @return array */ public static function sumOfProvider(): array { $items = [ ['invoice' => ['total' => 100]], ['invoice' => ['total' => 200]], ]; $floatItems = [ ['invoice' => ['total' => 100.0]], ['invoice' => ['total' => 200.0]], ]; return [ 'array' => [$items, 300], 'iterator' => [self::yieldItems($items), 300], 'floatArray' => [$floatItems, 300.0], 'floatIterator' => [self::yieldItems($floatItems), 300.0], ]; } /** * Tests the sumOf method * * @param float|int $expected */ #[DataProvider('sumOfProvider')] public function testSumOf(iterable $items, $expected): void { $this->assertEquals($expected, (new Collection($items))->sumOf('invoice.total')); } /** * Tests the sumOf method * * @param float|int $expected */ #[DataProvider('sumOfProvider')] public function testSumOfCallable(iterable $items, $expected): void { $sum = (new Collection($items))->sumOf(function ($v) { return $v['invoice']['total']; }); $this->assertEquals($expected, $sum); } /** * Tests the stopWhen method with a callable */ #[DataProvider('simpleProvider')] public function testStopWhenCallable(iterable $items): void { $collection = (new Collection($items))->stopWhen(function ($v) { return $v > 3; }); $this->assertEquals(['a' => 1, 'b' => 2, 'c' => 3], $collection->toArray()); } /** * Tests the stopWhen method with a matching array */ public function testStopWhenWithArray(): void { $items = [ ['foo' => 'bar'], ['foo' => 'baz'], ['foo' => 'foo'], ]; $collection = (new Collection($items))->stopWhen(['foo' => 'baz']); $this->assertEquals([['foo' => 'bar']], $collection->toArray()); } /** * Tests the unfold method */ public function testUnfold(): void { $items = [ [1, 2, 3, 4], [5, 6], [7, 8], ]; $collection = (new Collection($items))->unfold(); $this->assertEquals(range(1, 8), $collection->toArray(false)); $items = [ [1, 2], new Collection([3, 4]), ]; $collection = (new Collection($items))->unfold(); $this->assertEquals(range(1, 4), $collection->toArray(false)); } /** * Tests the unfold method with empty levels */ public function testUnfoldEmptyLevels(): void { $items = [[], [1, 2], []]; $collection = (new Collection($items))->unfold(); $this->assertEquals(range(1, 2), $collection->toArray(false)); $items = []; $collection = (new Collection($items))->unfold(); $this->assertEmpty($collection->toArray(false)); } /** * Tests the unfold when passing a callable */ public function testUnfoldWithCallable(): void { $items = [1, 2, 3]; $collection = (new Collection($items))->unfold(function ($item) { return range($item, $item * 2); }); $expected = [1, 2, 2, 3, 4, 3, 4, 5, 6]; $this->assertEquals($expected, $collection->toArray(false)); } /** * Tests the through() method */ public function testThrough(): void { $items = [1, 2, 3]; $collection = (new Collection($items))->through(function ($collection) { return $collection->append($collection->toList()); }); $this->assertEquals([1, 2, 3, 1, 2, 3], $collection->toList()); } /** * Tests the through method when it returns an array */ public function testThroughReturnArray(): void { $items = [1, 2, 3]; $collection = (new Collection($items))->through(function ($collection) { $list = $collection->toList(); return array_merge($list, $list); }); $this->assertEquals([1, 2, 3, 1, 2, 3], $collection->toList()); } /** * Tests that the sortBy method does not die when something that is not a * collection is passed */ public function testComplexSortBy(): void { $results = collection([3, 7]) ->unfold(function ($value) { return [ ['sorting' => $value * 2], ['sorting' => $value * 2], ]; }) ->sortBy('sorting') ->extract('sorting') ->toList(); $this->assertEquals([14, 14, 6, 6], $results); } /** * Tests __debugInfo() or debug() usage */ public function testDebug(): void { $items = [1, 2, 3]; $collection = new Collection($items); $result = $collection->__debugInfo(); $expected = [ 'count' => 3, ]; $this->assertSame($expected, $result); // Calling it again will rewind $result = $collection->__debugInfo(); $expected = [ 'count' => 3, ]; $this->assertSame($expected, $result); // Make sure it also works with non rewindable iterators $iterator = new NoRewindIterator(new ArrayIterator($items)); $collection = new Collection($iterator); $result = $collection->__debugInfo(); $expected = [ 'count' => 3, ]; $this->assertSame($expected, $result); // Calling it again will in this case not rewind $result = $collection->__debugInfo(); $expected = [ 'count' => 0, ]; $this->assertSame($expected, $result); $filter = function ($value): void { throw new Exception('filter exception'); }; $iterator = new CallbackFilterIterator(new ArrayIterator($items), $filter); $collection = new Collection($iterator); $result = $collection->__debugInfo(); $expected = [ 'count' => 'An exception occurred while getting count', ]; $this->assertSame($expected, $result); } /** * Tests the isEmpty() method */ public function testIsEmpty(): void { $collection = new Collection([1, 2, 3]); $this->assertFalse($collection->isEmpty()); $collection = $collection->map(function () { return null; }); $this->assertFalse($collection->isEmpty()); $collection = $collection->filter(); $this->assertTrue($collection->isEmpty()); } /** * Tests the isEmpty() method does not consume data * from buffered iterators. */ public function testIsEmptyDoesNotConsume(): void { $array = new ArrayIterator([1, 2, 3]); $inner = new BufferedIterator($array); $collection = new Collection($inner); $this->assertFalse($collection->isEmpty()); $this->assertCount(3, $collection->toArray()); } /** * Tests the zip() method */ public function testZip(): void { $collection = new Collection([1, 2]); $zipped = $collection->zip([3, 4]); $this->assertEquals([[1, 3], [2, 4]], $zipped->toList()); $collection = new Collection([1, 2]); $zipped = $collection->zip([3]); $this->assertEquals([[1, 3]], $zipped->toList()); $collection = new Collection([1, 2]); $zipped = $collection->zip([3, 4], [5, 6], [7, 8], [9, 10, 11]); $this->assertEquals([ [1, 3, 5, 7, 9], [2, 4, 6, 8, 10], ], $zipped->toList()); } /** * Tests the zipWith() method */ public function testZipWith(): void { $collection = new Collection([1, 2]); $zipped = $collection->zipWith([3, 4], function ($a, $b) { return $a * $b; }); $this->assertEquals([3, 8], $zipped->toList()); $zipped = $collection->zipWith([3, 4], [5, 6, 7], function (...$args) { return array_sum($args); }); $this->assertEquals([9, 12], $zipped->toList()); } /** * Tests the skip() method */ public function testSkip(): void { $collection = new Collection([1, 2, 3, 4, 5]); $this->assertEquals([3, 4, 5], $collection->skip(2)->toList()); $this->assertEquals([1, 2, 3, 4, 5], $collection->skip(0)->toList()); $this->assertEquals([4, 5], $collection->skip(3)->toList()); $this->assertEquals([5], $collection->skip(4)->toList()); } /** * Test skip() with an overflow */ public function testSkipOverflow(): void { $collection = new Collection([1, 2, 3]); $this->assertEquals([], $collection->skip(3)->toArray()); $this->assertEquals([], $collection->skip(4)->toArray()); } /** * Tests the skip() method with a traversable non-iterator */ public function testSkipWithTraversableNonIterator(): void { $collection = new Collection($this->datePeriod('2017-01-01', '2017-01-07')); $result = $collection->skip(3)->toList(); $expected = [ new DateTime('2017-01-04'), new DateTime('2017-01-05'), new DateTime('2017-01-06'), ]; $this->assertEquals($expected, $result); } /** * Tests the first() method with a traversable non-iterator */ public function testFirstWithTraversableNonIterator(): void { $collection = new Collection($this->datePeriod('2017-01-01', '2017-01-07')); $date = $collection->first(); $this->assertInstanceOf('DateTime', $date); $this->assertSame('2017-01-01', $date->format('Y-m-d')); } /** * Tests the last() method */ public function testLast(): void { $collection = new Collection([1, 2, 3]); $this->assertSame(3, $collection->last()); $collection = $collection->map(function ($e) { return $e * 2; }); $this->assertSame(6, $collection->last()); } /** * Tests the last() method when on an empty collection */ public function testLastWithEmptyCollection(): void { $collection = new Collection([]); $this->assertNull($collection->last()); } /** * Tests the last() method with a countable object */ public function testLastWithCountable(): void { $collection = new Collection(new ArrayObject([1, 2, 3])); $this->assertSame(3, $collection->last()); } /** * Tests the last() method with an empty countable object */ public function testLastWithEmptyCountable(): void { $collection = new Collection(new ArrayObject([])); $this->assertNull($collection->last()); } /** * Tests the last() method with a non-rewindable iterator */ public function testLastWithNonRewindableIterator(): void { $iterator = new NoRewindIterator(new ArrayIterator([1, 2, 3])); $collection = new Collection($iterator); $this->assertSame(3, $collection->last()); } /** * Tests the last() method with a traversable non-iterator */ public function testLastWithTraversableNonIterator(): void { $collection = new Collection($this->datePeriod('2017-01-01', '2017-01-07')); $date = $collection->last(); $this->assertInstanceOf('DateTime', $date); $this->assertSame('2017-01-06', $date->format('Y-m-d')); } /** * Tests the takeLast() method * * @param iterable $data The data to test with. */ #[DataProvider('simpleProvider')] public function testLastN($data): void { $collection = new Collection($data); $result = $collection->takeLast(3)->toArray(); $expected = ['b' => 2, 'c' => 3, 'd' => 4]; $this->assertEquals($expected, $result); } /** * Tests the takeLast() method with overflow * * @param iterable $data The data to test with. */ #[DataProvider('simpleProvider')] public function testLastNtWithOverflow($data): void { $collection = new Collection($data); $result = $collection->takeLast(10)->toArray(); $expected = ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]; $this->assertEquals($expected, $result); } /** * Tests the takeLast() with an odd numbers collection * * @param iterable $data The data to test with. */ #[DataProvider('simpleProvider')] public function testLastNtWithOddData($data): void { $collection = new Collection($data); $result = $collection->take(3)->takeLast(2)->toArray(); $expected = ['b' => 2, 'c' => 3]; $this->assertEquals($expected, $result); } /** * Tests the takeLast() with countable collection */ public function testLastNtWithCountable(): void { $rangeZeroToFive = range(0, 5); $collection = new Collection(new CountableIterator($rangeZeroToFive)); $result = $collection->takeLast(2)->toList(); $this->assertEquals([4, 5], $result); $collection = new Collection(new CountableIterator($rangeZeroToFive)); $result = $collection->takeLast(1)->toList(); $this->assertEquals([5], $result); } /** * Tests the takeLast() with countable collection * * @param iterable $data The data to test with. */ #[DataProvider('simpleProvider')] public function testLastNtWithNegative($data): void { $collection = new Collection($data); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The takeLast method requires a number greater than 0.'); $collection->takeLast(-1)->toArray(); } /** * Tests sumOf with no parameters */ public function testSumOfWithIdentity(): void { $collection = new Collection([1, 2, 3]); $this->assertSame(6, $collection->sumOf()); $collection = new Collection(['a' => 1, 'b' => 4, 'c' => 6]); $this->assertSame(11, $collection->sumOf()); } /** * Tests using extract with the {*} notation */ public function testUnfoldedExtract(): void { $items = [ ['comments' => [['id' => 1], ['id' => 2]]], ['comments' => [['id' => 3], ['id' => 4]]], ['comments' => [['id' => 7], ['nope' => 8]]], ]; $extracted = (new Collection($items))->extract('comments.{*}.id'); $this->assertEquals([1, 2, 3, 4, 7, null], $extracted->toArray()); $items = [ [ 'comments' => [ [ 'voters' => [['id' => 1], ['id' => 2]], ], ], ], [ 'comments' => [ [ 'voters' => [['id' => 3], ['id' => 4]], ], ], ], [ 'comments' => [ [ 'voters' => [['id' => 5], ['nope' => 'fail'], ['id' => 6]], ], ], ], [ 'comments' => [ [ 'not_voters' => [['id' => 5]], ], ], ], ['not_comments' => []], ]; $extracted = (new Collection($items))->extract('comments.{*}.voters.{*}.id'); $expected = [1, 2, 3, 4, 5, null, 6]; $this->assertEquals($expected, $extracted->toArray()); $this->assertEquals($expected, $extracted->toList()); } /** * Tests serializing a simple collection */ public function testSerializeSimpleCollection(): void { $collection = new Collection([1, 2, 3]); $serialized = serialize($collection); $unserialized = unserialize($serialized); $this->assertEquals($collection->toList(), $unserialized->toList()); $this->assertEquals($collection->toArray(), $unserialized->toArray()); } /** * Tests serialization when using append */ public function testSerializeWithAppendIterators(): void { $collection = new Collection([1, 2, 3]); $collection = $collection->append(['a' => 4, 'b' => 5, 'c' => 6]); $serialized = serialize($collection); $unserialized = unserialize($serialized); $this->assertEquals($collection->toList(), $unserialized->toList()); $this->assertEquals($collection->toArray(), $unserialized->toArray()); } /** * Tests serialization when using nested iterators */ public function testSerializeWithNestedIterators(): void { $collection = new Collection([1, 2, 3]); $collection = $collection->map(function ($e) { return $e * 3; }); $collection = $collection->groupBy(function ($e) { return $e % 2; }); $serialized = serialize($collection); $unserialized = unserialize($serialized); $this->assertEquals($collection->toList(), $unserialized->toList()); $this->assertEquals($collection->toArray(), $unserialized->toArray()); } /** * Tests serializing a zip() call */ public function testSerializeWithZipIterator(): void { $collection = new Collection([4, 5]); $collection = $collection->zip([1, 2]); $serialized = serialize($collection); $unserialized = unserialize($serialized); $this->assertEquals($collection->toList(), $unserialized->toList()); } /** * Provider for some chunk tests * * @return array */ public static function chunkProvider(): array { $items = range(1, 10); return [ 'array' => [$items], 'iterator' => [self::yieldItems($items)], ]; } /** * Tests the chunk method with exact chunks */ #[DataProvider('chunkProvider')] public function testChunk(iterable $items): void { $collection = new Collection($items); $chunked = $collection->chunk(2)->toList(); $expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10]]; $this->assertEquals($expected, $chunked); } /** * Tests the chunk method with overflowing chunk size */ public function testChunkOverflow(): void { $collection = new Collection(range(1, 11)); $chunked = $collection->chunk(2)->toList(); $expected = [[1, 2], [3, 4], [5, 6], [7, 8], [9, 10], [11]]; $this->assertEquals($expected, $chunked); } /** * Tests the chunk method with non-scalar items */ public function testChunkNested(): void { $collection = new Collection([1, 2, 3, [4, 5], 6, [7, [8, 9], 10], 11]); $chunked = $collection->chunk(2)->toList(); $expected = [[1, 2], [3, [4, 5]], [6, [7, [8, 9], 10]], [11]]; $this->assertEquals($expected, $chunked); } /** * Tests the chunkWithKeys method with exact chunks */ public function testChunkWithKeys(): void { $collection = new Collection(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6]); $chunked = $collection->chunkWithKeys(2)->toList(); $expected = [['a' => 1, 'b' => 2], ['c' => 3, 'd' => 4], ['e' => 5, 'f' => 6]]; $this->assertEquals($expected, $chunked); } /** * Tests the chunkWithKeys method with overflowing chunk size */ public function testChunkWithKeysOverflow(): void { $collection = new Collection(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7]); $chunked = $collection->chunkWithKeys(2)->toList(); $expected = [['a' => 1, 'b' => 2], ['c' => 3, 'd' => 4], ['e' => 5, 'f' => 6], ['g' => 7]]; $this->assertEquals($expected, $chunked); } /** * Tests the chunkWithKeys method with non-scalar items */ public function testChunkWithKeysNested(): void { $collection = new Collection(['a' => 1, 'b' => 2, 'c' => 3, 'd' => [4, 5], 'e' => 6, 'f' => [7, [8, 9], 10], 'g' => 11]); $chunked = $collection->chunkWithKeys(2)->toList(); $expected = [['a' => 1, 'b' => 2], ['c' => 3, 'd' => [4, 5]], ['e' => 6, 'f' => [7, [8, 9], 10]], ['g' => 11]]; $this->assertEquals($expected, $chunked); } /** * Tests the chunkWithKeys method without preserving keys */ public function testChunkWithKeysNoPreserveKeys(): void { $collection = new Collection(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, 'f' => 6, 'g' => 7]); $chunked = $collection->chunkWithKeys(2, false)->toList(); $expected = [[0 => 1, 1 => 2], [0 => 3, 1 => 4], [0 => 5, 1 => 6], [0 => 7]]; $this->assertEquals($expected, $chunked); } /** * Tests cartesianProduct */ public function testCartesianProduct(): void { $collection = new Collection([]); $result = $collection->cartesianProduct(); $expected = []; $this->assertEquals($expected, $result->toList()); $collection = new Collection([['A', 'B', 'C'], [1, 2, 3]]); $result = $collection->cartesianProduct(); $expected = [ ['A', 1], ['A', 2], ['A', 3], ['B', 1], ['B', 2], ['B', 3], ['C', 1], ['C', 2], ['C', 3], ]; $this->assertEquals($expected, $result->toList()); $collection = new Collection([[1, 2, 3], ['A', 'B', 'C'], ['a', 'b', 'c']]); $result = $collection->cartesianProduct(function ($value) { return [strval($value[0]) . $value[1] . $value[2]]; }, function ($value) { return $value[0] >= 2; }); $expected = [ ['2Aa'], ['2Ab'], ['2Ac'], ['2Ba'], ['2Bb'], ['2Bc'], ['2Ca'], ['2Cb'], ['2Cc'], ['3Aa'], ['3Ab'], ['3Ac'], ['3Ba'], ['3Bb'], ['3Bc'], ['3Ca'], ['3Cb'], ['3Cc'], ]; $this->assertEquals($expected, $result->toList()); $collection = new Collection([['1', '2', '3', '4'], ['A', 'B', 'C'], ['name', 'surname', 'telephone']]); $result = $collection->cartesianProduct(function ($value) { return [$value[0] => [$value[1] => $value[2]]]; }, function ($value) { return $value[2] !== 'surname'; }); $expected = [ [1 => ['A' => 'name']], [1 => ['A' => 'telephone']], [1 => ['B' => 'name']], [1 => ['B' => 'telephone']], [1 => ['C' => 'name']], [1 => ['C' => 'telephone']], [2 => ['A' => 'name']], [2 => ['A' => 'telephone']], [2 => ['B' => 'name']], [2 => ['B' => 'telephone']], [2 => ['C' => 'name']], [2 => ['C' => 'telephone']], [3 => ['A' => 'name']], [3 => ['A' => 'telephone']], [3 => ['B' => 'name']], [3 => ['B' => 'telephone']], [3 => ['C' => 'name']], [3 => ['C' => 'telephone']], [4 => ['A' => 'name']], [4 => ['A' => 'telephone']], [4 => ['B' => 'name']], [4 => ['B' => 'telephone']], [4 => ['C' => 'name']], [4 => ['C' => 'telephone']], ]; $this->assertEquals($expected, $result->toList()); $collection = new Collection([ [ 'name1' => 'alex', 'name2' => 'kostas', 0 => 'leon', ], [ 'val1' => 'alex@example.com', 24 => 'kostas@example.com', 'val2' => 'leon@example.com', ], ]); $result = $collection->cartesianProduct(); $expected = [ ['alex', 'alex@example.com'], ['alex', 'kostas@example.com'], ['alex', 'leon@example.com'], ['kostas', 'alex@example.com'], ['kostas', 'kostas@example.com'], ['kostas', 'leon@example.com'], ['leon', 'alex@example.com'], ['leon', 'kostas@example.com'], ['leon', 'leon@example.com'], ]; $this->assertEquals($expected, $result->toList()); } /** * Tests that an exception is thrown if the cartesian product is called with multidimensional arrays */ public function testCartesianProductMultidimensionalArray(): void { $this->expectException(LogicException::class); $collection = new Collection([ [ 'names' => [ 'alex', 'kostas', 'leon', ], ], [ 'locations' => [ 'crete', 'london', 'paris', ], ], ]); $collection->cartesianProduct(); } public function testTranspose(): void { $collection = new Collection([ ['Products', '2012', '2013', '2014'], ['Product A', '200', '100', '50'], ['Product B', '300', '200', '100'], ['Product C', '400', '300', '200'], ['Product D', '500', '400', '300'], ]); $transposed = $collection->transpose(); $expected = [ ['Products', 'Product A', 'Product B', 'Product C', 'Product D'], ['2012', '200', '300', '400', '500'], ['2013', '100', '200', '300', '400'], ['2014', '50', '100', '200', '300'], ]; $this->assertEquals($expected, $transposed->toList()); } /** * Tests that provided arrays do not have even length */ public function testTransposeUnEvenLengthShouldThrowException(): void { $this->expectException(LogicException::class); $collection = new Collection([ ['Products', '2012', '2013', '2014'], ['Product A', '200', '100', '50'], ['Product B', '300'], ['Product C', '400', '300'], ]); $collection->transpose(); } /** * Yields all the elements as passed * * @param iterable $items the elements to be yielded * @return \Generator */ protected static function yieldItems(iterable $items): Generator { foreach ($items as $k => $v) { yield $k => $v; } } /** * Create a DatePeriod object. * * @param string $start Start date * @param string $end End date */ protected function datePeriod($start, $end): DatePeriod { return new DatePeriod(new DateTime($start), new DateInterval('P1D'), new DateTime($end)); } /** * Tests that elements in a lazy collection are not fetched immediately. */ public function testLazy(): void { $items = ['a' => 1, 'b' => 2, 'c' => 3]; $collection = (new Collection($items))->lazy(); $callable = new class { public function __invoke(): never { throw new Exception('This should not be called'); } }; $collection->filter($callable)->filter($callable); $this->assertTrue(true); } }