From 8cdf2cfdb98d8759ec318c00f7ca56ba8ee4ef8f Mon Sep 17 00:00:00 2001 From: Jose Lorenzo Rodriguez Date: Thu, 18 Dec 2014 20:37:58 +0100 Subject: [PATCH] Implemented Collection::stopWhen() This is usuful when using collecitons for processing large files or infinite iterators/generators --- src/Collection/CollectionInterface.php | 35 ++++++++++ src/Collection/CollectionTrait.php | 30 ++++----- src/Collection/ExtractTrait.php | 27 ++++++++ src/Collection/Iterator/StoppableIterator.php | 67 +++++++++++++++++++ tests/TestCase/Collection/CollectionTest.php | 28 ++++++++ 5 files changed, 171 insertions(+), 16 deletions(-) create mode 100644 src/Collection/Iterator/StoppableIterator.php diff --git a/src/Collection/CollectionInterface.php b/src/Collection/CollectionInterface.php index d706920283e..e640329ceff 100644 --- a/src/Collection/CollectionInterface.php +++ b/src/Collection/CollectionInterface.php @@ -730,4 +730,39 @@ public function buffered(); */ public function listNested($dir = 'desc', $nestingKey = 'children'); +/** + * Creates a new collection that when iterated will stop yielding results if + * the provided condition evaluates to false. + * + * This is handy for dealing with infinite iterators or any generator that + * could start returning invalid elements at a certain point. For example, + * when reading lines from a file stream you may want to stop the iteration + * after a certain value is reached. + * + * ### Example: + * + * Get an array of lines in a CSV file until the timestamp column is less than a date + * + * {{{ + * $lines = (new Collection($fileLines))->stopWhen(function ($value, $key) { + * return (new DateTime($value))->format('Y') < 2012; + * }) + * ->toArray(); + * }}} + * + * Get elements until the first unapproved message is found: + * + * {{{ + * $comments = (new Collection($comments))->stopWhen(['is_approved' => false]); + * }}} + * + * @param callable|array $condition the method that will receive each of the elements and + * returns false when the iteration should be stopped. + * If an array, it will be interpreted as a key-value list of conditions where + * the key is a property path as accepted by `Collection::extract, + * and the value the condition against with each element will be matched. + * @return \Cake\Collection\CollectionInterface + */ + public function stopWhen($condition); + } diff --git a/src/Collection/CollectionTrait.php b/src/Collection/CollectionTrait.php index ba77c2edd66..101bbcc7c89 100644 --- a/src/Collection/CollectionTrait.php +++ b/src/Collection/CollectionTrait.php @@ -25,6 +25,7 @@ use Cake\Collection\Iterator\NestIterator; use Cake\Collection\Iterator\ReplaceIterator; use Cake\Collection\Iterator\SortIterator; +use Cake\Collection\Iterator\StoppableIterator; use Cake\Collection\Iterator\TreeIterator; use LimitIterator; @@ -254,22 +255,7 @@ public function take($size = 1, $from = 0) { * */ public function match(array $conditions) { - $matchers = []; - foreach ($conditions as $property => $value) { - $extractor = $this->_propertyExtractor($property); - $matchers[] = function ($v) use ($extractor, $value) { - return $extractor($v) == $value; - }; - } - - $filter = function ($value) use ($matchers) { - $valid = true; - foreach ($matchers as $match) { - $valid = $valid && $match($value); - } - return $valid; - }; - return $this->filter($filter); + return $this->filter($this->_createMatcherFilter($conditions)); } /** @@ -441,4 +427,16 @@ public function listNested($dir = 'desc', $nestingKey = 'children') { ); } +/** + * {@inheritDoc} + * + * @return \Cake\Collection\Iterator\StoppableIterator + */ + public function stopWhen($condition) { + if (!is_callable($condition)) { + $condition = $this->_createMatcherFilter($condition); + } + return new StoppableIterator($this, $condition); + } + } diff --git a/src/Collection/ExtractTrait.php b/src/Collection/ExtractTrait.php index 525e56b07a2..e3b4cdae2d7 100644 --- a/src/Collection/ExtractTrait.php +++ b/src/Collection/ExtractTrait.php @@ -60,4 +60,31 @@ protected function _extract($data, $path) { return $value; } +/** + * Returns a callable that receives a value and will return whether or not + * it matches certain condition. + * + * @param array $conditions A key-value list of conditions to match where the + * key is the property path to get from the current item and the value is the + * value to be compared the item with. + * @return callable + */ + protected function _createMatcherFilter(array $conditions) { + $matchers = []; + foreach ($conditions as $property => $value) { + $extractor = $this->_propertyExtractor($property); + $matchers[] = function ($v) use ($extractor, $value) { + return $extractor($v) == $value; + }; + } + + return function ($value) use ($matchers) { + $valid = true; + foreach ($matchers as $match) { + $valid = $valid && $match($value); + } + return $valid; + }; + } + } diff --git a/src/Collection/Iterator/StoppableIterator.php b/src/Collection/Iterator/StoppableIterator.php new file mode 100644 index 00000000000..ca6e64386e7 --- /dev/null +++ b/src/Collection/Iterator/StoppableIterator.php @@ -0,0 +1,67 @@ +_condition = $condition; + parent::__construct($items); + } + +/** + * Evaluates the condition and returns its result, this controls + * whther or not more results are yielded + * + * @return bool + */ + public function valid() { + if (!parent::valid()) { + return false; + } + + $current = $this->current(); + $key = $this->key(); + $condition = $this->_condition; + return !$condition($current, $key, $this); + } + +} diff --git a/tests/TestCase/Collection/CollectionTest.php b/tests/TestCase/Collection/CollectionTest.php index 0c641c482e1..0e3a88b6c71 100644 --- a/tests/TestCase/Collection/CollectionTest.php +++ b/tests/TestCase/Collection/CollectionTest.php @@ -967,4 +967,32 @@ public function testSumOf() { $this->assertEquals(600, $sum); } +/** + * Tests the stopWhen method with a callable + * + * @return void + */ + public function testStopWhenCallable() { + $items = [10, 20, 40, 10, 5]; + $collection = (new Collection($items))->stopWhen(function ($v) { + return $v > 20; + }); + $this->assertEquals([10, 20], $collection->toArray()); + } + +/** + * Tests the stopWhen method with a matching array + * + * @return void + */ + public function testStopWhenWithArray() { + $items = [ + ['foo' => 'bar'], + ['foo' => 'baz'], + ['foo' => 'foo'] + ]; + $collection = (new Collection($items))->stopWhen(['foo' => 'baz']); + $this->assertEquals([['foo' => 'bar']], $collection->toArray()); + } + }