浏览代码

Merge pull request #12534 from cakephp/collection-last-n

Implemented a new takeLast() method in Collection
José Lorenzo Rodríguez 7 年之前
父节点
当前提交
ec33e6f7ae
共有 3 个文件被更改,包括 222 次插入1 次删除
  1. 19 0
      src/Collection/CollectionInterface.php
  2. 99 0
      src/Collection/CollectionTrait.php
  3. 104 1
      tests/TestCase/Collection/CollectionTest.php

+ 19 - 0
src/Collection/CollectionInterface.php

@@ -558,6 +558,25 @@ interface CollectionInterface extends Iterator, JsonSerializable
     public function take($size = 1, $from = 0);
 
     /**
+     * Returns the last N elements of a collection
+     *
+     * ### Example:
+     *
+     * ```
+     * $items = [1, 2, 3, 4, 5];
+     *
+     * $last = (new Collection($items))->takeLast(3);
+     *
+     * // Result will look like this when converted to array
+     * [3, 4, 5];
+     * ```
+     *
+     * @param int $howMany The number of elements at the end of the collection
+     * @return \Cake\Collection\CollectionInterface
+     */
+    public function takeLast($howMany);
+
+    /**
      * Returns a new collection that will skip the specified amount of elements
      * at the beginning of the iteration.
      *

+ 99 - 0
src/Collection/CollectionTrait.php

@@ -402,6 +402,105 @@ trait CollectionTrait
     /**
      * {@inheritDoc}
      */
+    public function takeLast($howMany)
+    {
+        if ($howMany < 1) {
+            throw new \InvalidArgumentException("The takeLast method requires a number greater than 0.");
+        }
+
+        $iterator = $this->optimizeUnwrap();
+        if (is_array($iterator)) {
+            return new Collection(array_slice($iterator, $howMany * -1));
+        }
+
+        if ($iterator instanceof Countable) {
+            $count = count($iterator);
+
+            if ($count === 0) {
+                return new Collection([]);
+            }
+
+            $iterator = new LimitIterator($iterator, max(0, $count - $howMany), $howMany);
+
+            return new Collection($iterator);
+        }
+
+        $generator = function ($iterator, $howMany) {
+            $result = [];
+            $bucket = 0;
+            $offset = 0;
+
+            /**
+             * Consider the collection of elements [1, 2, 3, 4, 5, 6, 7, 8, 9], in order
+             * to get the last 4 elements, we can keep a buffer of 4 elements and
+             * fill it circularly using modulo logic, we use the $bucket variable
+             * to track the position to fill next in the buffer. This how the buffer
+             * looks like after 4 iterations:
+             *
+             * 0) 1 2 3 4 -- $bucket now goes back to 0, we have filled 4 elementes
+             * 1) 5 2 3 4 -- 5th iteration
+             * 2) 5 6 3 4 -- 6th iteration
+             * 3) 5 6 7 4 -- 7th iteration
+             * 4) 5 6 7 8 -- 8th iteration
+             * 5) 9 6 7 8
+             *
+             *  We can see that at the end of the iterations, the buffer contains all
+             *  the last four elements, just in the wrong order. How do we keep the
+             *  original order? Well, it turns out that the number of iteration also
+             *  give us a clue on what's going on, Let's add a marker for it now:
+             *
+             * 0) 1 2 3 4
+             *    ^ -- The 0) above now becomes the $offset variable
+             * 1) 5 2 3 4
+             *      ^ -- $offset = 1
+             * 2) 5 6 3 4
+             *        ^ -- $offset = 2
+             * 3) 5 6 7 4
+             *          ^ -- $offset = 3
+             * 4) 5 6 7 8
+             *    ^  -- We use module logic for $offset too
+             *          and as you can see each time $offset is 0, then the buffer
+             *          is sorted exactly as we need.
+             * 5) 9 6 7 8
+             *      ^ -- $offset = 1
+             *
+             * The $offset variable is a marker for splitting the buffer in two,
+             * elements to the right for the marker are the head of the final result,
+             * whereas the elements at the left are the tail. For example consider step 5)
+             * which has an offset of 1:
+             *
+             * - $head = elements to the right = [6, 7, 8]
+             * - $tail = elements to the left =  [9]
+             * - $result = $head + $tail = [6, 7, 8, 9]
+             *
+             * The logic above applies to collections of any size.
+             */
+
+            foreach ($iterator as $k => $item) {
+                $result[$bucket] = [$k, $item];
+                $bucket = (++$bucket) % $howMany;
+                $offset++;
+            }
+
+            $offset = $offset % $howMany;
+            $head = array_slice($result, $offset);
+            $tail = array_slice($result, 0, $offset);
+
+            foreach ($head as $v) {
+                yield $v[0] => $v[1];
+            }
+
+            foreach ($tail as $v) {
+                yield $v[0] => $v[1];
+            }
+        };
+
+        return new Collection($generator($iterator, $howMany));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
     public function append($items)
     {
         $list = new AppendIterator();

+ 104 - 1
tests/TestCase/Collection/CollectionTest.php

@@ -63,8 +63,28 @@ class TestIterator extends ArrayIterator
     }
 }
 
+class CountableIterator extends \IteratorIterator implements \Countable
+{
+    public function __construct($items)
+    {
+        $f = function () use ($items) {
+            foreach ($items as $e) {
+                yield $e;
+            }
+        };
+        parent::__construct($f());
+    }
+
+    public function count()
+    {
+        return 6;
+    }
+}
+
 /**
- * CollectionTest
+ * Collection Test
+ *
+ * @coversDefaultClass \Cake\Collection\Collection
  */
 class CollectionTest extends TestCase
 {
@@ -2151,6 +2171,89 @@ class CollectionTest extends TestCase
     }
 
     /**
+     * Tests the takeLast() method
+     *
+     * @dataProvider simpleProvider
+     * @param array $data The data to test with.
+     * @return void
+     * @covers ::takeLast
+     */
+    public function testLastN($data)
+    {
+        $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
+     *
+     * @dataProvider simpleProvider
+     * @param array $data The data to test with.
+     * @return void
+     * @covers ::takeLast
+     */
+    public function testLastNtWithOverflow($data)
+    {
+        $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
+     *
+     * @dataProvider simpleProvider
+     * @param array $data The data to test with.
+     * @return void
+     * @covers ::takeLast
+     */
+    public function testLastNtWithOddData($data)
+    {
+        $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
+     *
+     * @return void
+     * @covers ::takeLast
+     */
+    public function testLastNtWithCountable()
+    {
+        $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
+     *
+     * @dataProvider simpleProvider
+     * @param array $data The data to test with.
+     * @return void
+     * @covers ::takeLast
+     */
+    public function testLastNtWithNegative($data)
+    {
+        $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
      *
      * @return void