Skip to content

Commit

Permalink
Implemented Collection::stopWhen()
Browse files Browse the repository at this point in the history
This is usuful when using collecitons for processing large files or
infinite iterators/generators
  • Loading branch information
lorenzo committed Dec 18, 2014
1 parent 1c4aa96 commit 8cdf2cf
Show file tree
Hide file tree
Showing 5 changed files with 171 additions and 16 deletions.
35 changes: 35 additions & 0 deletions src/Collection/CollectionInterface.php
Expand Up @@ -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);

}
30 changes: 14 additions & 16 deletions src/Collection/CollectionTrait.php
Expand Up @@ -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;

Expand Down Expand Up @@ -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));
}

/**
Expand Down Expand Up @@ -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);
}

}
27 changes: 27 additions & 0 deletions src/Collection/ExtractTrait.php
Expand Up @@ -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;
};
}

}
67 changes: 67 additions & 0 deletions src/Collection/Iterator/StoppableIterator.php
@@ -0,0 +1,67 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice.
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.0.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Collection\Iterator;

use Cake\Collection\Collection;

/**
* Creates an iterator from another iterator that will verify a condition on each
* step. If the condition evaluates to false, the iterator will not yield more
* results.
*/
class StoppableIterator extends Collection {

/**
* The condition to evaluate for each item of the collection
*
* @var callable
*/
protected $_condition;

/**
* Creates an iterator that can be stopped based on a condition provided by a callback.
*
* Each time the condition callback is executed it will receive the value of the element
* in the current iteration, the key of the element and the passed $items iterator
* as arguments, in that order.
*
* @param array|\Traversable $items The list of values to iterate
* @param callable $condition A function that will be called for ech item in
* the collection, if the result evaluates to false, no more items will be
* yielded from this iterator.
*/
public function __construct($items, callable $condition) {
$this->_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);
}

}
28 changes: 28 additions & 0 deletions tests/TestCase/Collection/CollectionTest.php
Expand Up @@ -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());
}

}

0 comments on commit 8cdf2cf

Please sign in to comment.