From 4be33548469b417bebbdb034562877d29eb9f453 Mon Sep 17 00:00:00 2001 From: Joseph Silber Date: Mon, 21 Jun 2021 09:42:28 -0400 Subject: [PATCH] Add collection `sliding` method (#37751) --- src/Illuminate/Collections/Collection.php | 16 +++++ src/Illuminate/Collections/LazyCollection.php | 39 ++++++++++++ tests/Support/SupportCollectionTest.php | 59 +++++++++++++++++++ .../SupportLazyCollectionIsLazyTest.php | 23 ++++++++ 4 files changed, 137 insertions(+) diff --git a/src/Illuminate/Collections/Collection.php b/src/Illuminate/Collections/Collection.php index 74e5f9d5053e..d1e94b2cd322 100644 --- a/src/Illuminate/Collections/Collection.php +++ b/src/Illuminate/Collections/Collection.php @@ -959,6 +959,22 @@ public function shuffle($seed = null) return new static(Arr::shuffle($this->items, $seed)); } + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param int $size + * @param int $step + * @return static + */ + public function sliding($size = 2, $step = 1) + { + $chunks = floor(($this->count() - $size) / $step) + 1; + + return static::times($chunks, function ($number) use ($size, $step) { + return $this->slice(($number - 1) * $step, $size); + }); + } + /** * Skip the first {$count} items. * diff --git a/src/Illuminate/Collections/LazyCollection.php b/src/Illuminate/Collections/LazyCollection.php index 5b2f19f8add7..43f176e21a5b 100644 --- a/src/Illuminate/Collections/LazyCollection.php +++ b/src/Illuminate/Collections/LazyCollection.php @@ -920,6 +920,45 @@ public function shuffle($seed = null) return $this->passthru('shuffle', func_get_args()); } + /** + * Create chunks representing a "sliding window" view of the items in the collection. + * + * @param int $size + * @param int $step + * @return static + */ + public function sliding($size = 2, $step = 1) + { + return new static(function () use ($size, $step) { + $iterator = $this->getIterator(); + + $chunk = []; + + while ($iterator->valid()) { + $chunk[$iterator->key()] = $iterator->current(); + + if (count($chunk) == $size) { + yield tap(new static($chunk), function () use (&$chunk, $step) { + $chunk = array_slice($chunk, $step, null, true); + }); + + // If the $step between chunks is bigger than each chunk's $size, + // we will skip the extra items (which should never be in any + // chunk) before we continue to the next chunk in the loop. + if ($step > $size) { + $skip = $step - $size; + + for ($i = 0; $i < $skip && $iterator->valid(); $i++) { + $iterator->next(); + } + } + } + + $iterator->next(); + } + }); + } + /** * Skip the first {$count} items. * diff --git a/tests/Support/SupportCollectionTest.php b/tests/Support/SupportCollectionTest.php index 15fb2f4c247f..fa07495a6db6 100755 --- a/tests/Support/SupportCollectionTest.php +++ b/tests/Support/SupportCollectionTest.php @@ -235,6 +235,65 @@ public function testShiftReturnsAndRemovesFirstItemInCollection() $this->assertNull($data->first()); } + /** + * @dataProvider collectionClassProvider + */ + public function testSliding($collection) + { + // Default parameters: $size = 2, $step = 1 + $this->assertSame([], $collection::times(0)->sliding()->toArray()); + $this->assertSame([], $collection::times(1)->sliding()->toArray()); + $this->assertSame([[1, 2]], $collection::times(2)->sliding()->toArray()); + $this->assertSame( + [[1, 2], [2, 3]], + $collection::times(3)->sliding()->map->values()->toArray() + ); + + // Custom step: $size = 2, $step = 3 + $this->assertSame([], $collection::times(1)->sliding(2, 3)->toArray()); + $this->assertSame([[1, 2]], $collection::times(2)->sliding(2, 3)->toArray()); + $this->assertSame([[1, 2]], $collection::times(3)->sliding(2, 3)->toArray()); + $this->assertSame([[1, 2]], $collection::times(4)->sliding(2, 3)->toArray()); + $this->assertSame( + [[1, 2], [4, 5]], + $collection::times(5)->sliding(2, 3)->map->values()->toArray() + ); + + // Custom size: $size = 3, $step = 1 + $this->assertSame([], $collection::times(2)->sliding(3)->toArray()); + $this->assertSame([[1, 2, 3]], $collection::times(3)->sliding(3)->toArray()); + $this->assertSame( + [[1, 2, 3], [2, 3, 4]], + $collection::times(4)->sliding(3)->map->values()->toArray() + ); + $this->assertSame( + [[1, 2, 3], [2, 3, 4]], + $collection::times(4)->sliding(3)->map->values()->toArray() + ); + + // Custom size and custom step: $size = 3, $step = 2 + $this->assertSame([], $collection::times(2)->sliding(3, 2)->toArray()); + $this->assertSame([[1, 2, 3]], $collection::times(3)->sliding(3, 2)->toArray()); + $this->assertSame([[1, 2, 3]], $collection::times(4)->sliding(3, 2)->toArray()); + $this->assertSame( + [[1, 2, 3], [3, 4, 5]], + $collection::times(5)->sliding(3, 2)->map->values()->toArray() + ); + $this->assertSame( + [[1, 2, 3], [3, 4, 5]], + $collection::times(6)->sliding(3, 2)->map->values()->toArray() + ); + + // Ensure keys are preserved, and inner chunks are also collections + $chunks = $collection::times(3)->sliding(); + + $this->assertSame([[0 => 1, 1 => 2], [1 => 2, 2 => 3]], $chunks->toArray()); + + $this->assertInstanceOf($collection, $chunks); + $this->assertInstanceOf($collection, $chunks->first()); + $this->assertInstanceOf($collection, $chunks->skip(1)->first()); + } + /** * @dataProvider collectionClassProvider */ diff --git a/tests/Support/SupportLazyCollectionIsLazyTest.php b/tests/Support/SupportLazyCollectionIsLazyTest.php index dca52b769e07..54d8ea60cae7 100644 --- a/tests/Support/SupportLazyCollectionIsLazyTest.php +++ b/tests/Support/SupportLazyCollectionIsLazyTest.php @@ -897,6 +897,29 @@ public function testShuffleIsLazy() }); } + public function testSlidingIsLazy() + { + $this->assertDoesNotEnumerate(function ($collection) { + $collection->sliding(); + }); + + $this->assertEnumerates(2, function ($collection) { + $collection->sliding()->take(1)->all(); + }); + + $this->assertEnumerates(3, function ($collection) { + $collection->sliding()->take(2)->all(); + }); + + $this->assertEnumerates(13, function ($collection) { + $collection->sliding(3, 5)->take(3)->all(); + }); + + $this->assertEnumeratesOnce(function ($collection) { + $collection->sliding()->all(); + }); + } + public function testSkipIsLazy() { $this->assertDoesNotEnumerate(function ($collection) {