Browse Source

Implemented Collection::stopWhen()

This is usuful when using collecitons for processing large files or
infinite iterators/generators
Jose Lorenzo Rodriguez 11 years ago
parent
commit
8cdf2cfdb9

+ 35 - 0
src/Collection/CollectionInterface.php

@@ -730,4 +730,39 @@ interface CollectionInterface extends Iterator, JsonSerializable {
  */
 	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);
+
 }

+ 14 - 16
src/Collection/CollectionTrait.php

@@ -25,6 +25,7 @@ use Cake\Collection\Iterator\MapReduce;
 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 @@ trait CollectionTrait {
  *
  */
 	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 @@ trait CollectionTrait {
 		);
 	}
 
+/**
+ * {@inheritDoc}
+ *
+ * @return \Cake\Collection\Iterator\StoppableIterator
+ */
+	public function stopWhen($condition) {
+		if (!is_callable($condition)) {
+			$condition = $this->_createMatcherFilter($condition);
+		}
+		return new StoppableIterator($this, $condition);
+	}
+
 }

+ 27 - 0
src/Collection/ExtractTrait.php

@@ -60,4 +60,31 @@ trait ExtractTrait {
 		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 - 0
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 - 0
tests/TestCase/Collection/CollectionTest.php

@@ -967,4 +967,32 @@ class CollectionTest extends TestCase {
 		$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());
+	}
+
 }