├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── library └── Xi │ └── Collections │ ├── Collection.php │ ├── Collection │ ├── AbstractCollection.php │ ├── ArrayCollection.php │ ├── OuterCollection.php │ ├── SimpleCollection.php │ └── SimpleCollectionView.php │ ├── CollectionView.php │ ├── Enumerable.php │ ├── Enumerable │ ├── AbstractEnumerable.php │ ├── ArrayEnumerable.php │ ├── OuterEnumerable.php │ └── SimpleEnumerable.php │ └── Util │ ├── CallbackFilterIterator.php │ ├── CallbackMapIterator.php │ ├── FlatMapIterator.php │ ├── Functions.php │ ├── LazyIterator.php │ └── ReindexIterator.php └── tests ├── Xi └── Collections │ ├── Collection │ ├── AbstractCollectionTest.php │ ├── ArrayCollectionTest.php │ ├── ArrayCollectionViewTest.php │ ├── OuterCollectionTest.php │ ├── OuterCollectionViewTest.php │ ├── SimpleCollectionTest.php │ └── SimpleCollectionViewTest.php │ └── Enumerable │ ├── AbstractEnumerableTest.php │ ├── ArrayEnumerableTest.php │ ├── OuterEnumerableTest.php │ └── SimpleEnumerableTest.php ├── bootstrap.php └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | /nbproject 2 | /.idea 3 | /tests/phpunit.xml 4 | /tests/coverage 5 | /vendor/ 6 | composer.phar 7 | composer.lock 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | before_script: 9 | - wget http://getcomposer.org/composer.phar 10 | - php composer.phar install 11 | 12 | script: phpunit -c tests 13 | 14 | notifications: 15 | irc: "irc.freenode.net#xi-project" 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Mikko Forsström, Mikko Hirvonen, Eevert Saukkokoski 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | * Neither the names of the authors nor the names of contributors may be used 15 | to endorse or promote products derived from this software without specific 16 | prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY DIRECT, INDIRECT, 22 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 24 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF 27 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Xi Collections 2 | 3 | Functional, immutable and extensible enumerations and collections for PHP 5.3. 4 | 5 | ## Design Philosophy 6 | 7 | PHP has always lacked solid collections support, with the vast majority of programmers making do with arrays and the related built-in functions. With the introduction of SPL in PHP 5.0 and the consequent extensions in 5.3, there are currently more choices than ever if all you want for is speed and answers to specific use cases. Array processing, however, is not significantly better than ten years ago, with the API about as comfortable and handy for everyday tasks as picking at your dinner with a shovel. 8 | 9 | Xi Collections aims to rectify the situation and inject your workflow with a hearty dose of functional and declarative aspects. This is intended to result in more clarity in expressing and understanding processing collections of objects or data, allowing you to work faster and deliver more self-documenting code. 10 | 11 | ## Design Principles 12 | 13 | - _Object immutability._ The collections' methods do not manipulate the collections' contents, but return a new collection instead. 14 | 15 | - _API chainability._ Most operations will return a new collection except specific reduces and size informations. 16 | 17 | - _Embracing functional programming._ The Collections API is chosen to facilitate a functional workflow. Existing FP-compatible functionality in PHP is prioritized for inclusion to the API, and other important concepts that are missing are stolen from other languages and libraries. 18 | 19 | - _Out-of-the-box extensibility._ Decorators for separating the concrete Collection implementations from your modifications to the API are included. The interfaces tend to the minimal rather than extensive, making new implementations easier. Specifically, the whole of PHP's array functions is not built-in, but can be readily used if you need to. 20 | 21 | # Examples 22 | 23 | ## From imperative to functional 24 | 25 | Let's assume a simple loop that filters and transforms a set of data: 26 | 27 | public function getMatchingInterestingParts() { 28 | $result = array(); 29 | foreach ($this->getFoos() as $key => $value) { 30 | if ($this->match($value)) { 31 | $result[$key] = $value->getInterestingParts(); 32 | } 33 | } 34 | return $result; 35 | } 36 | 37 | Here's the same expressed with Collections: 38 | 39 | public function getMatchingInterestingParts() { 40 | return $this->getFoos() 41 | ->filter(function(Foo $foo) { 42 | return $this->matches($foo); 43 | })->map(function(Foo $foo) { 44 | return $foo->getInterestingParts(); 45 | }); 46 | } 47 | 48 | The latter bit of code is not much shorter, and for someone unfamiliar with functional constructs it may be more difficult to process. It does, however, have a few interesting qualities. The code communicates its intent better - filter values, then map the result, and nothing else. This is especially beneficial when considering code with a significantly more complex set of transformations. There's less room for error; index associations are automatically maintained. This also means you can focus on the interesting bits instead of boilerplate, which helps both when reading and when writing the code. A third benefit is that you can take full advantage of type hints and the safety they can bring, something which will be lacking with a simple foreach loop. 49 | 50 | ## Simplifying common access patterns 51 | 52 | One of the most common use cases for looping over an array is collecting the results of a member access or method invocation from every item. Collections makes that easy. 53 | 54 | public function getBarsByFoos() { 55 | $bars = array(); 56 | foreach ($this->getFoos() as $key => $foo) { 57 | $bars[$key] = $foo->getBar(); 58 | } 59 | return $bars; 60 | } 61 | // becomes 62 | public function getBarsByFoos() { 63 | return $this->getFoos()->invoke('getBar'); 64 | } 65 | 66 | public function getFooTrivialities() { 67 | $trivialities = array(); 68 | foreach ($this->getFoos() as $key => $foo) { 69 | $trivialities[$key] = $foo->triviality; 70 | } 71 | return $trivialities; 72 | } 73 | // becomes 74 | public function getFooTrivialities() { 75 | return $this->getFoos()->pick('triviality'); 76 | } 77 | 78 | Picking even works for arrays (or objects implementing ArrayAccess) as well, and you don't need to care about which type the input is. 79 | 80 | ## Inspecting intermediate steps of complex operations 81 | 82 | Suppose you have a pipeline where data is transformed according to complex rules. 83 | 84 | public function getAliveQuxen() { 85 | return $this->getFoos() 86 | ->map($this->fromFooToBar) 87 | ->filter(function($bar) { return $bar->isAlive(); }) 88 | ->map($this->fromBarToQux); 89 | } 90 | 91 | Suppose further that you want to inspect the data as it passes from one step to another. This is where you'd introduce temporary variables, were the code imperatively structured. With Collections, all you need is `tap`. It accepts a function that takes the contents of the collection as its parameter - and does nothing but call the function. 92 | 93 | public function getAliveQuxen() { 94 | return $this->getFoos() 95 | ->map($this->fromFooToBar) 96 | ->filter(function($bar) { return $bar->isAlive(); }) 97 | ->tap(function($bars) { $this->log($bars); }) 98 | ->map($this->fromBarToQux); 99 | } 100 | 101 | A reader of your code will be able to immediately recognize that the part in `tap` is only being executed for its side effects and that it has nothing to do with the transformation itself. We could've used `each` in a similar fashion if we were instead interested in the individual units of computation. 102 | 103 | public function getAliveQuxen() { 104 | return $this->getFoos() 105 | ->map($this->fromFooToBar) 106 | ->filter(function($bar) { return $bar->isAlive(); }) 107 | ->each(function($bar) { $this->logBar($bar); }) 108 | ->map($this->fromBarToQux); 109 | } 110 | 111 | ## Delaying computation using views 112 | 113 | In some cases you may wish to expose a certain Collection to a consumer, but are not certain whether the Collection is going to be used, and generating one is potentially costly. In such a case you can apply a CollectionView, which is a set of transformation operations that haven't yet been applied to an underlying base Collection. Upon access, the operations will be applied and the resulting values provided to the consumer. 114 | 115 | A Collection is transformed into a view backed by itself by invoking `view`. Best effort is made to apply all Collection method calls lazily. Forcing the view into actual values happens when accessing any Enumerator methods. 116 | 117 | public function getEnormouslyExpensiveCollection() { 118 | return $this->getStuff()->view()->map(function(Stuff $s) { 119 | return enormouslyExpensiveComputation($s); 120 | }); 121 | } 122 | 123 | There's a caveat, however. It is not guaranteed that the transformation from lazy to strict should happen exactly once per CollectionView object. If you need that, you should `force` the view object to get a strict one. 124 | 125 | ## Using an extended API on the fly 126 | 127 | In any given PHP environment there tends to be an amount of existing functionality around for processing data in a Traversable format. PHP itself has a plethora of built-in array functions that aren't feasible to support in the Collection API if it is supposed to be kept minimal. This can potentially change with the introduction of traits in PHP 5.4, but for now you'll have to figure out ways to use these functions manually. At the core of this facility is `apply`. It accepts a function that applies a transformation of some kind to the collection, the result of which is taken in as a new collection. 128 | 129 | Let's assume you want to sort your values. Here's a way to do it using `apply`. 130 | 131 | public function getSortedFoos() { 132 | return $this->getFoos() 133 | ->apply(function($collection) { 134 | $foos = $collection->toArray(); 135 | ksort($foos); 136 | return $foos; 137 | }); 138 | } 139 | 140 | The argument is a collection, which will have to be converted to an array first to be accepted by `ksort`. The function also operates on references, not values, so a temporary variable is necessary. There's an amount of cruft with this use case, but you're likely to be using raw PHP functions rarely. If you're using `apply` with functions that have a more reasonable API, eg. accept Traversable objects instead of necessitating arrays, the footprint becomes much more palatable. In such a fictional scenario for `ksort`, for instance: 141 | 142 | public function getSortedFoos() { 143 | return $this->getFoos() 144 | ->apply('ksort'); 145 | } 146 | 147 | # API basics 148 | 149 | Collections has two core interfaces. `Enumerable` implements a set of collection operations relying only on traversability. `Collection` extends the `Enumerable` operations to a superset that includes operations that yield other collections in return. This means collections can be transformed into other collections. 150 | 151 | Every concrete class has a static `create` method that can be used for fluently constructing and accessing a collection. For instance: 152 | 153 | ArrayCollection::create($values)->invoke('getBar')->each(function(Bar $bar) { $bar->engage(); }); 154 | 155 | Below is a short description of the APIs provided by Enumerable and Collection. For more thorough information, you'll need to consult the source. 156 | 157 | ## Enumerable 158 | 159 | ### Element retrieval 160 | 161 | - `first`: Returns the first element in the collection 162 | - `last`: Returns the last element in the collection 163 | - `find`: Returns the first value that satisfies a given predicate 164 | 165 | ### Element conditions 166 | 167 | - `exists`: Checks whether the collection has at least one element satisfying a given predicate 168 | - `forAll`: Checks whether all of the elements in the collection satisfy a given predicate 169 | - `countAll`: Counts the amount of elements in the collection that satisfy a given predicate 170 | 171 | ### Size information 172 | 173 | - `count`: Counts the amount of elements in the collection 174 | 175 | ### Reduces 176 | 177 | - `reduce`: Uses a given callback to reduce the collection's elements to a single value, starting from a provided initial value 178 | 179 | ### Invocations 180 | 181 | - `tap`: Calls a provided callback with this object as a parameter 182 | - `each`: Performs an operation once per key-value pair 183 | 184 | ## Collection 185 | 186 | ### Maps 187 | 188 | - `map`: Applies a callback for each value-key-pair in the Collection and returns a new one with values replaced by the return values from the callback 189 | - `flatMap`: Applies a callback for each key-value-pair in the Collection assuming that the callback result value is iterable and returns a new one with values from those iterable 190 | - `pick`: Get a Collection with a key or member property picked from each value 191 | - `values`: Get a Collection with just the values from this Collection 192 | - `keys`: Get a Collection with the keys from this one as values 193 | - `invoke`: Map this Collection by invoking a method on every value 194 | - `apply`: Creates a new Collection of this type from the output of a given callback that takes this Collection as its argument 195 | 196 | ### Subcollections 197 | 198 | - `take`: Creates a new Collection with up to $number first elements from this one 199 | - `rest`: Creates a new Collection with the rest of the elements except first 200 | - `filter`: Creates a Collection with the values of this collection that match a given predicate 201 | - `filterNot`: Creates a collection with the values of this collection that do not match a given predicate 202 | - `unique`: Get a Collection with only the unique values from this one 203 | 204 | ### Subdivisions 205 | 206 | - `partition`: Split a collection into a pair of two collections; one with elements that match a given predicate, the other with the elements that do not. 207 | - `groupBy`: Group the values in the Collection into nested Collections according to a given callback 208 | 209 | ### Additions 210 | 211 | - `concatenate`: Creates a Collection with elements from this and another one 212 | - `union`: Creates a Collection with key-value pairs in the `$other` Collection overriding ones in `$this` Collection 213 | - `flatten`: Flatten nested arrays and Traversables 214 | - `add`: Get a new Collection with given value and optionally key appended 215 | 216 | ### Size information 217 | 218 | - `isEmpty`: Checks whether the collection is empty 219 | 220 | ### Sorts 221 | 222 | - `indexBy`: Reindex the Collection using a given callback 223 | - `sortWith`: Get this Collection sorted with a given comparison function 224 | - `sortBy`: Get this Collection sorted with a given metric 225 | 226 | ### Views 227 | 228 | - `view`: Provides a Collection where transformer operations are applied lazily 229 | 230 | ### Specific reduces 231 | 232 | - `min`: Returns the minimum value in the collection 233 | - `max`: Returns the maximum value in the collection 234 | - `sum`: Returns the sum of values in the collection 235 | - `product`: Returns the product of values in the collection 236 | 237 | ## CollectionView 238 | 239 | - `force`: Coerces this view back into the underlying Collection type 240 | 241 | ## Collection implementations 242 | 243 | - `ArrayCollection`: Basic Collection backed by a plain PHP array. 244 | - `OuterCollection`: A decorator for a Collection. Can easily be extended to provide more collection operations without locking down the implementation specifics. 245 | 246 | # Running the unit tests 247 | 248 | phpunit -c tests 249 | 250 | # TODO 251 | 252 | - Collection implementations backed by SPL (SplFixedArray, SplDoublyLinkedList?) 253 | - Use traits? 254 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xi/collections", 3 | "type": "library", 4 | "description": "Functional, immutable and extensible enumerations and collections for PHP 5.3.", 5 | "keywords": ["collections"], 6 | "homepage": "http://github.com/xi-project/xi-collections", 7 | "license": "BSD-3-Clause", 8 | "authors": [ 9 | { 10 | "name": "Eevert Saukkokoski", 11 | "email": "eevert.saukkokoski@gmail.com", 12 | "role": "Developer" 13 | }, 14 | { 15 | "name": "Mikko Hirvonen", 16 | "email": "mikko.petteri.hirvonen@gmail.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": ">=5.3.0" 22 | }, 23 | "autoload": { 24 | "psr-0": { 25 | "Xi\\Collections": ["library/", "tests/"] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /library/Xi/Collections/Collection.php: -------------------------------------------------------------------------------- 1 | $value) { 34 | if ($number-- <= 0) { 35 | break; 36 | } 37 | $results[$key] = $value; 38 | } 39 | return static::create($results); 40 | } 41 | 42 | public function map($callback) 43 | { 44 | $results = array(); 45 | foreach ($this as $key => $value) { 46 | $results[$key] = $callback($value, $key); 47 | } 48 | return static::create($results); 49 | } 50 | 51 | public function filter($predicate = null) 52 | { 53 | if (null === $predicate) { 54 | $predicate = $this->notEmptyFilter(); 55 | } 56 | 57 | $results = array(); 58 | foreach ($this as $key => $value) { 59 | if ($predicate($value, $key)) { 60 | $results[$key] = $value; 61 | } 62 | } 63 | return static::create($results); 64 | } 65 | 66 | /** 67 | * @return callable 68 | */ 69 | private function notEmptyFilter() 70 | { 71 | return function ($value) { 72 | return !empty($value); 73 | }; 74 | } 75 | 76 | /** 77 | * {@inheritdoc} 78 | */ 79 | public function filterNot($predicate = null) 80 | { 81 | if (null === $predicate) { 82 | $predicate = $this->notEmptyFilter(); 83 | } 84 | 85 | $results = array(); 86 | 87 | foreach ($this as $key => $value) { 88 | if (!$predicate($value, $key)) { 89 | $results[$key] = $value; 90 | } 91 | } 92 | 93 | return static::create($results); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function partition($predicate) 100 | { 101 | return static::create(array($this->filter($predicate), $this->filterNot($predicate))); 102 | } 103 | 104 | public function concatenate($other) 105 | { 106 | $results = array(); 107 | foreach ($this as $value) { 108 | $results[] = $value; 109 | } 110 | foreach ($other as $value) { 111 | $results[] = $value; 112 | } 113 | return static::create($results); 114 | } 115 | 116 | public function union($other) 117 | { 118 | $results = $this->toArray(); 119 | foreach ($other as $key => $value) { 120 | $results[$key] = $value; 121 | } 122 | return static::create($results); 123 | } 124 | 125 | public function values() 126 | { 127 | $results = array(); 128 | foreach ($this as $value) { 129 | $results[] = $value; 130 | } 131 | return static::create($results); 132 | } 133 | 134 | public function keys() 135 | { 136 | $results = array(); 137 | foreach ($this as $key => $value) { 138 | $results[] = $key; 139 | } 140 | return static::create($results); 141 | } 142 | 143 | public function flatMap($callback) 144 | { 145 | return $this->apply(Functions::flatMap($callback)); 146 | } 147 | 148 | public function indexBy($callback) 149 | { 150 | return $this->apply(Functions::indexBy($callback)); 151 | } 152 | 153 | public function groupBy($callback) 154 | { 155 | return $this->apply(Functions::groupBy($callback, $this->getCreator())); 156 | } 157 | 158 | public function pick($key) 159 | { 160 | return $this->map(Functions::pick($key)); 161 | } 162 | 163 | public function invoke($method) 164 | { 165 | return $this->map(Functions::invoke($method)); 166 | } 167 | 168 | public function flatten() 169 | { 170 | return $this->apply(Functions::flatten()); 171 | } 172 | 173 | public function unique($strict = true) 174 | { 175 | return $this->apply(Functions::unique($strict)); 176 | } 177 | 178 | public function sortWith($comparator) 179 | { 180 | return $this->apply(Functions::sortWith($comparator)); 181 | } 182 | 183 | public function sortBy($metric) 184 | { 185 | return $this->apply(Functions::sortBy($metric)); 186 | } 187 | 188 | /** 189 | * {@inheritdoc} 190 | */ 191 | public function add($value, $key = null) 192 | { 193 | $results = $this->toArray(); 194 | 195 | if ($key === null) { 196 | $results[] = $value; 197 | } else { 198 | $results[$key] = $value; 199 | } 200 | 201 | return static::create($results); 202 | } 203 | 204 | /** 205 | * {@inheritdoc} 206 | */ 207 | public function min() 208 | { 209 | if ($this->isEmpty()) { 210 | throw new UnderflowException( 211 | 'Can not get a minimum value on an empty collection.' 212 | ); 213 | } 214 | 215 | $min = null; 216 | 217 | foreach ($this as $value) { 218 | if ($min === null || $value < $min) { 219 | $min = $value; 220 | } 221 | } 222 | 223 | return $min; 224 | } 225 | 226 | /** 227 | * {@inheritdoc} 228 | */ 229 | public function max() 230 | { 231 | if ($this->isEmpty()) { 232 | throw new UnderflowException( 233 | 'Can not get a maximum value on an empty collection.' 234 | ); 235 | } 236 | 237 | $max = null; 238 | 239 | foreach ($this as $value) { 240 | if ($max === null || $value > $max) { 241 | $max = $value; 242 | } 243 | } 244 | 245 | return $max; 246 | } 247 | 248 | /** 249 | * {@inheritdoc} 250 | */ 251 | public function sum() 252 | { 253 | $sum = 0; 254 | 255 | foreach ($this as $value) { 256 | $sum += $value; 257 | } 258 | 259 | return $sum; 260 | } 261 | 262 | /** 263 | * {@inheritdoc} 264 | */ 265 | public function product() 266 | { 267 | $product = 1; 268 | 269 | foreach ($this as $value) { 270 | $product *= $value; 271 | } 272 | 273 | return $product; 274 | } 275 | 276 | /** 277 | * {@inheritdoc} 278 | */ 279 | public function rest() 280 | { 281 | $results = array(); 282 | $first = true; 283 | 284 | foreach ($this as $key => $value) { 285 | if (!$first) { 286 | $results[$key] = $value; 287 | } 288 | 289 | $first = false; 290 | } 291 | 292 | return static::create($results); 293 | } 294 | 295 | /** 296 | * {@inheritdoc} 297 | */ 298 | public function isEmpty() 299 | { 300 | return $this->count() === 0; 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /library/Xi/Collections/Collection/ArrayCollection.php: -------------------------------------------------------------------------------- 1 | 0) { 49 | $result = array_slice($this->elements, 0, $number, true); 50 | } 51 | return static::create($result); 52 | } 53 | 54 | /** 55 | * {@inheritdoc} 56 | */ 57 | public function rest() 58 | { 59 | return static::create(array_slice($this->elements, 1, null, true)); 60 | } 61 | 62 | public function filter($callback = null) 63 | { 64 | // Passing null to array_filter results in error, but omitting the second argument is ok. 65 | $result = (null === $callback) 66 | ? array_filter($this->elements) 67 | // array_filter only provides values; adding keys manually 68 | : array_filter($this->elements, $this->addKeyArgument($callback)); 69 | return static::create($result); 70 | } 71 | 72 | /** 73 | * {@inheritdoc} 74 | */ 75 | public function filterNot($predicate = null) 76 | { 77 | if (null === $predicate) { 78 | $predicate = function ($value) { 79 | return !empty($value); 80 | }; 81 | } 82 | 83 | $results = array(); 84 | 85 | foreach ($this as $key => $value) { 86 | if (!$predicate($value, $key)) { 87 | $results[$key] = $value; 88 | } 89 | } 90 | 91 | return static::create($results); 92 | } 93 | 94 | /** 95 | * {@inheritdoc} 96 | */ 97 | public function partition($predicate) 98 | { 99 | return static::create(array($this->filter($predicate), $this->filterNot($predicate))); 100 | } 101 | 102 | public function map($callback) 103 | { 104 | // Providing keys to the callback manually, because index associations 105 | // are not maintained when array_map is called with multiple arrays. 106 | return static::create(array_map($this->addKeyArgument($callback), $this->elements)); 107 | } 108 | 109 | /** 110 | * Wraps a callback that accepts a value-key pair as its arguments into a 111 | * callback that only accepts the value and retrieves a key from the 112 | * elements manually for each call. Can be used when a function that accepts 113 | * a callback and iterates through the elements will only provide values to 114 | * the passed callback. 115 | * 116 | * TODO: Perform analysis on whether it would be altogether more sensible to 117 | * implement filter and map manually, if providing a consistent interface 118 | * while taking advantage of PHP functions means resorting tricks like this. 119 | * 120 | * @param callback($value, $key) $callback 121 | * @return callback($value) 122 | */ 123 | private function addKeyArgument($callback) 124 | { 125 | $values = $this->elements; 126 | return function($value) use($callback, &$values) { 127 | list($key) = each($values); 128 | return $callback($value, $key); 129 | }; 130 | } 131 | 132 | public function concatenate($other) 133 | { 134 | $left = array_values($this->elements); 135 | $right = array_values($other->toArray()); 136 | return static::create(array_merge($left, $right)); 137 | } 138 | 139 | public function union($other) 140 | { 141 | return static::create($other->toArray() + $this->elements); 142 | } 143 | 144 | public function values() 145 | { 146 | return static::create(array_values($this->elements)); 147 | } 148 | 149 | public function keys() 150 | { 151 | return static::create(array_keys($this->elements)); 152 | } 153 | 154 | public function flatMap($callback) 155 | { 156 | return $this->apply(Functions::flatMap($callback)); 157 | } 158 | 159 | public function indexBy($callback) 160 | { 161 | return $this->apply(Functions::indexBy($callback)); 162 | } 163 | 164 | public function groupBy($callback) 165 | { 166 | return $this->apply(Functions::groupBy($callback, $this->getCreator())); 167 | } 168 | 169 | public function pick($key) 170 | { 171 | return $this->map(Functions::pick($key)); 172 | } 173 | 174 | public function invoke($method) 175 | { 176 | return $this->map(Functions::invoke($method)); 177 | } 178 | 179 | public function flatten() 180 | { 181 | return $this->apply(Functions::flatten()); 182 | } 183 | 184 | public function unique($strict = true) 185 | { 186 | if (false === $strict) { 187 | // array_unique can't check for strict uniqueness 188 | return static::create(array_unique($this->elements, SORT_REGULAR)); 189 | } 190 | return $this->apply(Functions::unique($strict)); 191 | } 192 | 193 | public function sortWith($comparator) 194 | { 195 | return $this->apply(Functions::sortWith($comparator)); 196 | } 197 | 198 | public function sortBy($metric) 199 | { 200 | return $this->apply(Functions::sortBy($metric)); 201 | } 202 | 203 | /** 204 | * @return ArrayCollection 205 | */ 206 | public function reverse() 207 | { 208 | return static::create(array_reverse($this->elements)); 209 | } 210 | 211 | /** 212 | * @param Collection $other 213 | * @return ArrayCollection 214 | */ 215 | public function merge(Collection $other) 216 | { 217 | return static::create(array_merge($this->elements, $other->toArray())); 218 | } 219 | 220 | /** 221 | * {@inheritdoc} 222 | */ 223 | public function add($value, $key = null) 224 | { 225 | $results = $this->toArray(); 226 | 227 | if ($key === null) { 228 | $results[] = $value; 229 | } else { 230 | $results[$key] = $value; 231 | } 232 | 233 | return static::create($results); 234 | } 235 | 236 | /** 237 | * {@inheritdoc} 238 | */ 239 | public function min() 240 | { 241 | if ($this->isEmpty()) { 242 | throw new UnderflowException( 243 | 'Can not get a minimum value on an empty collection.' 244 | ); 245 | } 246 | 247 | return min($this->elements); 248 | } 249 | 250 | /** 251 | * {@inheritdoc} 252 | */ 253 | public function max() 254 | { 255 | if ($this->isEmpty()) { 256 | throw new UnderflowException( 257 | 'Can not get a maximum value on an empty collection.' 258 | ); 259 | } 260 | 261 | return max($this->elements); 262 | } 263 | 264 | /** 265 | * {@inheritdoc} 266 | */ 267 | public function sum() 268 | { 269 | return $this->isEmpty() ? 0 : array_sum($this->elements); 270 | } 271 | 272 | /** 273 | * {@inheritdoc} 274 | */ 275 | public function product() 276 | { 277 | return $this->isEmpty() ? 1 : array_product($this->elements); 278 | } 279 | 280 | /** 281 | * {@inheritdoc} 282 | */ 283 | public function isEmpty() 284 | { 285 | return $this->count() === 0; 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /library/Xi/Collections/Collection/OuterCollection.php: -------------------------------------------------------------------------------- 1 | collection = $collection; 27 | } 28 | 29 | /** 30 | * @return Collection 31 | */ 32 | public function getInnerCollection() 33 | { 34 | return $this->collection; 35 | } 36 | 37 | /** 38 | * @param Collection $elements 39 | * @return OuterCollection 40 | * @throws InvalidArgumentException 41 | */ 42 | public static function create($elements) 43 | { 44 | if ($elements instanceof Collection) { 45 | return new static($elements); 46 | } 47 | throw new \InvalidArgumentException("OuterCollection can only wrap Collection instances"); 48 | } 49 | 50 | public static function getCreator() 51 | { 52 | return Functions::getCallback(get_called_class(), 'create'); 53 | } 54 | 55 | public function view() 56 | { 57 | return new SimpleCollectionView($this, static::getCreator()); 58 | } 59 | 60 | public function apply($callback) 61 | { 62 | return static::create($this->collection->apply($callback)); 63 | } 64 | 65 | public function take($number) 66 | { 67 | return static::create($this->collection->take($number)); 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | */ 73 | public function rest() 74 | { 75 | return static::create($this->collection->rest()); 76 | } 77 | 78 | public function map($callback) 79 | { 80 | return static::create($this->collection->map($callback)); 81 | } 82 | 83 | public function filter($predicate = null) 84 | { 85 | return static::create($this->collection->filter($predicate)); 86 | } 87 | 88 | /** 89 | * {@inheritdoc} 90 | */ 91 | public function filterNot($predicate = null) 92 | { 93 | return static::create($this->collection->filterNot($predicate)); 94 | } 95 | 96 | /** 97 | * {@inheritdoc} 98 | */ 99 | public function partition($predicate) 100 | { 101 | return static::create($this->collection->partition($predicate)); 102 | } 103 | 104 | public function concatenate($other) 105 | { 106 | return static::create($this->collection->concatenate($other)); 107 | } 108 | 109 | public function union($other) 110 | { 111 | return static::create($this->collection->union($other)); 112 | } 113 | 114 | public function values() 115 | { 116 | return static::create($this->collection->values()); 117 | } 118 | 119 | public function keys() 120 | { 121 | return static::create($this->collection->keys()); 122 | } 123 | 124 | public function flatMap($callback) 125 | { 126 | return static::create($this->collection->flatMap($callback)); 127 | } 128 | 129 | public function indexBy($callback) 130 | { 131 | return static::create($this->collection->indexBy($callback)); 132 | } 133 | 134 | public function groupBy($callback) 135 | { 136 | return static::create($this->collection->groupBy($callback)); 137 | } 138 | 139 | public function pick($key) 140 | { 141 | return static::create($this->collection->pick($key)); 142 | } 143 | 144 | public function invoke($method) 145 | { 146 | return static::create($this->collection->invoke($method)); 147 | } 148 | 149 | public function flatten() 150 | { 151 | return static::create($this->collection->flatten()); 152 | } 153 | 154 | public function unique($strict = true) 155 | { 156 | return static::create($this->collection->unique($strict)); 157 | } 158 | 159 | public function sortWith($comparator) 160 | { 161 | return static::create($this->collection->sortWith($comparator)); 162 | } 163 | 164 | public function sortBy($metric) 165 | { 166 | return static::create($this->collection->sortBy($metric)); 167 | } 168 | 169 | /** 170 | * {@inheritdoc} 171 | */ 172 | public function add($value, $key = null) 173 | { 174 | return static::create($this->collection->add($value, $key)); 175 | } 176 | 177 | /** 178 | * {@inheritdoc} 179 | */ 180 | public function min() 181 | { 182 | return $this->collection->min(); 183 | } 184 | 185 | /** 186 | * {@inheritdoc} 187 | */ 188 | public function max() 189 | { 190 | return $this->collection->max(); 191 | } 192 | 193 | /** 194 | * {@inheritdoc} 195 | */ 196 | public function sum() 197 | { 198 | return $this->collection->sum(); 199 | } 200 | 201 | /** 202 | * {@inheritdoc} 203 | */ 204 | public function product() 205 | { 206 | return $this->collection->product(); 207 | } 208 | 209 | /** 210 | * {@inheritdoc} 211 | */ 212 | public function isEmpty() 213 | { 214 | return $this->collection->isEmpty(); 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /library/Xi/Collections/Collection/SimpleCollection.php: -------------------------------------------------------------------------------- 1 | traversable = $traversable; 22 | } 23 | 24 | public function getIterator() 25 | { 26 | return Functions::getIterator($this->traversable); 27 | } 28 | 29 | public static function create($elements) 30 | { 31 | return new static($elements); 32 | } 33 | 34 | public static function getCreator() 35 | { 36 | return Functions::getCallback(get_called_class(), 'create'); 37 | } 38 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Collection/SimpleCollectionView.php: -------------------------------------------------------------------------------- 1 | traversable = $traversable; 20 | if (null === $creator) { 21 | $creator = SimpleCollection::getCreator(); 22 | } 23 | $this->creator = $creator; 24 | } 25 | 26 | public function force() 27 | { 28 | $creator = $this->creator; 29 | return $creator($this); 30 | } 31 | 32 | protected function lazy($iterator) 33 | { 34 | return new static($iterator, $this->creator); 35 | } 36 | 37 | public function apply($callback) 38 | { 39 | $self = $this; 40 | return $this->lazy($this->getLazyIteratorFor(function() use($self, $callback) { 41 | return $callback($self); 42 | })); 43 | } 44 | 45 | public function take($number) 46 | { 47 | if (0 >= $number) { 48 | return $this->lazy(new \EmptyIterator); 49 | } 50 | return $this->lazy(new \LimitIterator($this->getIterator(), 0, $number)); 51 | } 52 | 53 | public function map($callback) 54 | { 55 | return $this->lazy($this->getMapIteratorFor($this->getIterator(), $callback)); 56 | } 57 | 58 | public function filter($predicate = null) 59 | { 60 | if (null === $predicate) { 61 | $predicate = function($value) { 62 | return !empty($value); 63 | }; 64 | } 65 | 66 | return $this->lazy($this->getFilterIteratorFor($this->getIterator(), $predicate)); 67 | } 68 | 69 | public function concatenate($other) 70 | { 71 | $iterator = new \AppendIterator; 72 | $iterator->append($this->getIterator()); 73 | $iterator->append($this->getIteratorFor($other)); 74 | return $this->lazy($this->getReindexIteratorFor($iterator)); 75 | } 76 | 77 | public function union($other) 78 | { 79 | $iterator = new \AppendIterator; 80 | $iterator->append($this->getIterator()); 81 | $iterator->append($this->getIteratorFor($other)); 82 | return $this->lazy($this->getLazyIteratorFor(function() use($iterator) { 83 | return iterator_to_array($iterator); 84 | })); 85 | } 86 | 87 | public function flatMap($callback) 88 | { 89 | return $this->lazy($this->getReindexIteratorFor($this->getFlatMapIteratorFor($this->getIterator(), $callback))); 90 | } 91 | 92 | public function values() 93 | { 94 | return $this->lazy($this->getReindexIteratorFor($this->getIterator())); 95 | } 96 | 97 | public function keys() 98 | { 99 | return $this->lazy($this->getReindexIteratorFor($this->getMapIteratorFor( 100 | $this->getIterator(), 101 | function($v, $k) { return $k; } 102 | ))); 103 | } 104 | 105 | protected function getIteratorFor($other) 106 | { 107 | return Util\Functions::getIterator($other); 108 | } 109 | 110 | protected function getLazyIteratorFor($callback) 111 | { 112 | return new Util\LazyIterator($callback); 113 | } 114 | 115 | protected function getReindexIteratorFor($iterator) 116 | { 117 | return new Util\ReindexIterator($iterator); 118 | } 119 | 120 | protected function getFlatMapIteratorFor($iterator, $callback) 121 | { 122 | return new \RecursiveIteratorIterator(new Util\FlatMapIterator($iterator, $callback)); 123 | } 124 | 125 | protected function getMapIteratorFor($iterator, $callback) 126 | { 127 | return new Util\CallbackMapIterator($iterator, $callback); 128 | } 129 | 130 | protected function getFilterIteratorFor($iterator, $callback) 131 | { 132 | return new Util\CallbackFilterIterator($iterator, $callback); 133 | } 134 | } -------------------------------------------------------------------------------- /library/Xi/Collections/CollectionView.php: -------------------------------------------------------------------------------- 1 | getIterator()); 14 | } 15 | 16 | public function tap($callback) 17 | { 18 | $callback($this); 19 | return $this; 20 | } 21 | 22 | public function each($callback, $userdata = null) 23 | { 24 | foreach ($this as $key => $value) { 25 | $callback($value, $key, $userdata); 26 | } 27 | return $this; 28 | } 29 | 30 | public function reduce($callback, $initial = null) 31 | { 32 | $result = $initial; 33 | foreach ($this as $key => $value) { 34 | $result = $callback($result, $value, $key); 35 | } 36 | return $result; 37 | } 38 | 39 | public function find($predicate) 40 | { 41 | foreach ($this as $value) { 42 | if ($predicate($value)) { 43 | return $value; 44 | } 45 | } 46 | return null; 47 | } 48 | 49 | public function exists($predicate) 50 | { 51 | foreach ($this as $value) { 52 | if ($predicate($value)) { 53 | return true; 54 | } 55 | } 56 | return false; 57 | } 58 | 59 | public function forAll($predicate) 60 | { 61 | foreach ($this as $value) { 62 | if (!$predicate($value)) { 63 | return false; 64 | } 65 | } 66 | return true; 67 | } 68 | 69 | public function first() 70 | { 71 | foreach ($this as $value) { 72 | return $value; 73 | } 74 | return null; 75 | } 76 | 77 | public function last() 78 | { 79 | $result = null; 80 | foreach ($this as $value) { 81 | $result = $value; 82 | } 83 | return $result; 84 | } 85 | 86 | public function countAll($predicate) 87 | { 88 | $count = 0; 89 | foreach ($this as $value) { 90 | if ($predicate($value)) { 91 | $count++; 92 | } 93 | } 94 | return $count; 95 | } 96 | 97 | public function count() 98 | { 99 | return count($this->toArray()); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /library/Xi/Collections/Enumerable/ArrayEnumerable.php: -------------------------------------------------------------------------------- 1 | elements = $elements; 24 | } 25 | 26 | public function getIterator() 27 | { 28 | return new \ArrayIterator($this->elements); 29 | } 30 | 31 | public function toArray() 32 | { 33 | return $this->elements; 34 | } 35 | 36 | public function each($callback, $userdata = null) 37 | { 38 | array_walk($this->elements, $callback, $userdata); 39 | return $this; 40 | } 41 | 42 | public function reduce($callback, $initial = null) 43 | { 44 | $result = $initial; 45 | foreach ($this->elements as $key => $value) { 46 | $result = $callback($result, $value, $key); 47 | } 48 | return $result; 49 | } 50 | 51 | public function tap($callback) 52 | { 53 | $callback($this); 54 | return $this; 55 | } 56 | 57 | public function exists($callback) 58 | { 59 | foreach ($this->elements as $value) { 60 | if ($callback($value)) { 61 | return true; 62 | } 63 | } 64 | return false; 65 | } 66 | 67 | public function forAll($callback) 68 | { 69 | foreach ($this->elements as $value) { 70 | if (!$callback($value)) { 71 | return false; 72 | } 73 | } 74 | return true; 75 | } 76 | 77 | public function find($callback) 78 | { 79 | foreach ($this->elements as $value) { 80 | if ($callback($value)) { 81 | return $value; 82 | } 83 | } 84 | return null; 85 | } 86 | 87 | public function first() 88 | { 89 | return reset($this->elements); 90 | } 91 | 92 | public function last() 93 | { 94 | return end($this->elements); 95 | } 96 | 97 | public function countAll($predicate) 98 | { 99 | $count = 0; 100 | foreach ($this as $value) { 101 | if ($predicate($value)) { 102 | $count++; 103 | } 104 | } 105 | return $count; 106 | } 107 | 108 | public function count() 109 | { 110 | return count($this->elements); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /library/Xi/Collections/Enumerable/OuterEnumerable.php: -------------------------------------------------------------------------------- 1 | enumerable = $enumerable; 24 | } 25 | 26 | /** 27 | * @return Enumerable 28 | */ 29 | public function getInnerEnumerable() 30 | { 31 | return $this->enumerable; 32 | } 33 | 34 | public function toArray() 35 | { 36 | return $this->enumerable->toArray(); 37 | } 38 | 39 | public function tap($callback) 40 | { 41 | $callback($this); 42 | return $this; 43 | } 44 | 45 | public function each($callback, $userdata = null) 46 | { 47 | $this->enumerable->each($callback, $userdata); 48 | return $this; 49 | } 50 | 51 | public function reduce($callback, $initial = null) 52 | { 53 | return $this->enumerable->reduce($callback, $initial); 54 | } 55 | 56 | public function exists($predicate) 57 | { 58 | return $this->enumerable->exists($predicate); 59 | } 60 | 61 | public function find($predicate) 62 | { 63 | return $this->enumerable->find($predicate); 64 | } 65 | 66 | public function forAll($predicate) 67 | { 68 | return $this->enumerable->forAll($predicate); 69 | } 70 | 71 | public function getIterator() 72 | { 73 | return $this->enumerable->getIterator(); 74 | } 75 | 76 | public function first() 77 | { 78 | return $this->enumerable->first(); 79 | } 80 | 81 | public function last() 82 | { 83 | return $this->enumerable->last(); 84 | } 85 | 86 | public function count() 87 | { 88 | return $this->enumerable->count(); 89 | } 90 | 91 | public function countAll($predicate) 92 | { 93 | return $this->enumerable->countAll($predicate); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /library/Xi/Collections/Enumerable/SimpleEnumerable.php: -------------------------------------------------------------------------------- 1 | traversable = $traversable; 23 | } 24 | 25 | /** 26 | * @return \Iterator 27 | */ 28 | public function getIterator() 29 | { 30 | return Functions::getIterator($this->traversable); 31 | } 32 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Util/CallbackFilterIterator.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 15 | } 16 | 17 | public function accept() 18 | { 19 | $callback = $this->callback; 20 | return $callback($this->current(), $this->key()); 21 | } 22 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Util/CallbackMapIterator.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 15 | } 16 | 17 | public function current() 18 | { 19 | $callback = $this->callback; 20 | return $callback(parent::current(), $this->key()); 21 | } 22 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Util/FlatMapIterator.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 12 | } 13 | 14 | public function hasChildren() 15 | { 16 | return null !== $this->callback; 17 | } 18 | 19 | public function getChildren() 20 | { 21 | $callback = $this->callback; 22 | $children = $callback($this->current()); 23 | return new static($this->getIteratorFor($children)); 24 | } 25 | 26 | protected function getIteratorFor($other) 27 | { 28 | return Functions::getIterator($other); 29 | } 30 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Util/Functions.php: -------------------------------------------------------------------------------- 1 | getIterator(); 46 | case is_array($value): return new \ArrayIterator($value); 47 | case $value instanceof \Traversable: return new \IteratorIterator($value); 48 | default: throw new \InvalidArgumentException("Argument should be one of array, Traversable, IteratorAggregate, Iterator"); 49 | } 50 | } 51 | 52 | /** 53 | * @param boolean $strict optional, defaults to true 54 | * @return callback($collection) 55 | */ 56 | public static function unique($strict = true) 57 | { 58 | return function($collection) use($strict) { 59 | $result = array(); 60 | foreach ($collection as $key => $value) { 61 | if (!in_array($value, $result, $strict)) { 62 | $result[$key] = $value; 63 | } 64 | } 65 | return $result; 66 | }; 67 | } 68 | 69 | /** 70 | * @param scalar $key 71 | * @return callback(ArrayAccess|array|object|mixed) 72 | */ 73 | public static function pick($key) 74 | { 75 | return function($input) use($key) { 76 | switch(true) { 77 | case $input instanceof \ArrayAccess: 78 | case is_array($input): return isset($input[$key]) ? $input[$key] : null; 79 | case is_object($input): return isset($input->$key) ? $input->$key : null; 80 | default: return null; 81 | } 82 | }; 83 | } 84 | 85 | /** 86 | * @param string $method 87 | * @return callback(object) 88 | */ 89 | public static function invoke($method) 90 | { 91 | return function($object) use($method) { 92 | return $object->$method(); 93 | }; 94 | } 95 | 96 | /** 97 | * @param callback($value, $key) $callback 98 | * @return callback(Traversable) 99 | */ 100 | public static function flatMap($callback) 101 | { 102 | return function($collection) use($callback) { 103 | $results = array(); 104 | foreach ($collection as $key => $value) { 105 | foreach ($callback($value, $key) as $flattened) { 106 | $results[] = $flattened; 107 | } 108 | } 109 | return $results; 110 | }; 111 | } 112 | 113 | /** 114 | * @param callback($value, $key) $callback 115 | * @return callback(Traversable) 116 | */ 117 | public static function indexBy($callback) 118 | { 119 | return function($collection) use($callback) { 120 | $results = array(); 121 | foreach ($collection as $key => $value) { 122 | $results[$callback($value, $key)] = $value; 123 | } 124 | return $results; 125 | }; 126 | } 127 | 128 | /** 129 | * @param callback($value, $key) $groupIndex 130 | * @param callback($group) $groupValue optional 131 | * @return callback(Traversable) 132 | */ 133 | public static function groupBy($groupIndex, $groupValue = null) 134 | { 135 | return function($collection) use($groupIndex, $groupValue) { 136 | $results = array(); 137 | // Create groups using $groupIndex 138 | foreach ($collection as $key => $value) { 139 | $results[$groupIndex($value, $key)][] = $value; 140 | } 141 | // Transform groups using $groupValue 142 | if (null !== $groupValue) { 143 | foreach ($results as $key => $value) { 144 | $results[$key] = $groupValue($value); 145 | } 146 | } 147 | return $results; 148 | }; 149 | } 150 | 151 | /** 152 | * Flattens nested collections of arrays or Traversable objects 153 | * 154 | * @return callback(Traversable) 155 | */ 156 | public static function flatten() 157 | { 158 | $flatten = function($collection) use(&$flatten) { 159 | $results = array(); 160 | foreach ($collection as $value) { 161 | if (is_array($value) || ($value instanceof \Traversable)) { 162 | $results = array_merge($results, $flatten($value)); 163 | } else { 164 | $results[] = $value; 165 | } 166 | } 167 | return $results; 168 | }; 169 | return $flatten; 170 | } 171 | 172 | /** 173 | * @param callback($a, $b) $comparator 174 | * @return callback(Traversable) 175 | */ 176 | public static function sortWith($comparator) 177 | { 178 | return function($collection) use($comparator) { 179 | $values = $collection->toArray(); 180 | usort($values, $comparator); 181 | return $values; 182 | }; 183 | } 184 | 185 | /** 186 | * @param callback($value, $key) $metric 187 | * @return callback(Traversable) 188 | */ 189 | public static function sortBy($metric) 190 | { 191 | return self::sortWith(function($a, $b) use($metric) { 192 | $ma = $metric($a); 193 | $mb = $metric($b); 194 | if ($ma == $mb) { 195 | return 0; 196 | } 197 | return ($ma < $mb) ? -1 : 1; 198 | }); 199 | } 200 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Util/LazyIterator.php: -------------------------------------------------------------------------------- 1 | callback = $callback; 20 | } 21 | 22 | public function rewind() 23 | { 24 | if (!$this->initialized) { 25 | $callback = $this->callback; 26 | $this->append($this->getIteratorFor($callback())); 27 | $this->initialized = true; 28 | } 29 | parent::rewind(); 30 | } 31 | 32 | protected function getIteratorFor($value) 33 | { 34 | return Functions::getIterator($value); 35 | } 36 | } -------------------------------------------------------------------------------- /library/Xi/Collections/Util/ReindexIterator.php: -------------------------------------------------------------------------------- 1 | index = 0; 16 | } 17 | 18 | public function key() 19 | { 20 | return $this->index++; 21 | } 22 | } -------------------------------------------------------------------------------- /tests/Xi/Collections/Collection/AbstractCollectionTest.php: -------------------------------------------------------------------------------- 1 | getCollection($elements); 21 | } 22 | 23 | /** 24 | * @return mixed 25 | */ 26 | protected function unit() 27 | { 28 | return array(); 29 | } 30 | 31 | /** 32 | * @test 33 | */ 34 | public function creatorFunctionShouldBeLateStaticBound() 35 | { 36 | $collection = $this->getCollection(); 37 | $class = get_class($collection); 38 | $this->assertEquals(get_class($class::getCreator()->__invoke($this->unit())), $class); 39 | } 40 | 41 | /** 42 | * @test 43 | * @dataProvider mixedElements 44 | */ 45 | public function shouldBeAbleToTransformWholeCollectionUsingApply($elements) 46 | { 47 | $collection = $this->getCollection($elements); 48 | $result = $collection->apply(function ($c) { 49 | return array_flip($c->toArray()); 50 | }); 51 | $this->assertEquals($elements, array_flip($result->toArray())); 52 | } 53 | 54 | public function takeElements() 55 | { 56 | return array( 57 | array(array(), 0, array()), 58 | array(array(1), 0, array()), 59 | array(array(1), 1, array(1)), 60 | array(array(1, 2, 3), 3, array(1, 2, 3)), 61 | array(array(1, 2, 3), 4, array(1, 2, 3)), 62 | array(array(1, 2, 3), -1, array()) 63 | ); 64 | } 65 | 66 | /** 67 | * @test 68 | * @dataProvider takeElements 69 | */ 70 | public function shouldBeAbleToTakeNFirstElements($elements, $number, $expect) 71 | { 72 | $collection = $this->getCollection($elements); 73 | $result = $collection->take($number); 74 | $this->assertEquals($expect, $result->toArray()); 75 | } 76 | 77 | public function integerIncrementSet() 78 | { 79 | return array( 80 | array(array(), array()), 81 | array(array(1), array(2)), 82 | array(array(1, 2, 3), array(2, 3, 4)) 83 | ); 84 | } 85 | 86 | /** 87 | * @test 88 | * @dataProvider integerIncrementSet 89 | */ 90 | public function shouldBeAbleToMapEachElement($elements, $expected) 91 | { 92 | $collection = $this->getCollection($elements); 93 | $result = $collection->map(function ($v) { 94 | return $v + 1; 95 | }); 96 | $this->assertEquals($expected, $result->toArray()); 97 | } 98 | 99 | public function flatMapSet() 100 | { 101 | return array( 102 | array(array(), array()), 103 | array(array('f'), array('f')), 104 | array(array('foo'), array('f', 'o', 'o')), 105 | array(array('foo', 'bar'), array('f', 'o', 'o', 'b', 'a', 'r')), 106 | array(array('foo', 'ba', 'q'), array('f', 'o', 'o', 'b', 'a', 'q')) 107 | ); 108 | } 109 | 110 | /** 111 | * @test 112 | * @dataProvider flatMapSet 113 | */ 114 | public function shouldBeAbleToGetMapOutputAsFlat($elements, $expected) 115 | { 116 | $collection = $this->getCollection($elements); 117 | $result = $collection->flatMap(function($v) { return str_split($v); }); 118 | $this->assertEquals($expected, $result->toArray()); 119 | } 120 | 121 | public function indexedIntegerIncrementSet() 122 | { 123 | return array( 124 | array(array(), array()), 125 | array(array(1), array(2)), 126 | array(array('foo' => 1, 'bar' => 2), array('foo' => 2, 'bar' => 3)) 127 | ); 128 | } 129 | 130 | /** 131 | * @test 132 | * @dataProvider indexedIntegerIncrementSet 133 | * @depends shouldBeAbleToMapEachElement 134 | */ 135 | public function shouldMaintainIndexAssociationsWhenMapping($elements, $expected) 136 | { 137 | $collection = $this->getCollection($elements); 138 | $result = $collection->map(function ($v) { 139 | return $v + 1; 140 | }); 141 | $this->assertEquals($expected, $result->toArray()); 142 | } 143 | 144 | public function keyMapSet() 145 | { 146 | return array( 147 | array(array('foo' => null), array('foo' => 'foo')), 148 | array(array('min' => 1, 'max' => 3), array('min' => 'min', 'max' => 'max')), 149 | ); 150 | } 151 | 152 | /** 153 | * @test 154 | * @dataProvider keyMapSet 155 | * @depends shouldMaintainIndexAssociationsWhenMapping 156 | */ 157 | public function shouldProvideKeysToMapFunction($elements, $expected) 158 | { 159 | $collection = $this->getCollection($elements); 160 | $result = $collection->map(function($v, $k) { 161 | return $k; 162 | }); 163 | $this->assertEquals($expected, $result->toArray()); 164 | } 165 | 166 | public function truthinessSet() 167 | { 168 | return array( 169 | array(array(), array()), 170 | array(array(true, false), array(true)), 171 | array(array(0, 1, 2), array(1, 2)) 172 | ); 173 | } 174 | 175 | /** 176 | * @test 177 | * @dataProvider truthinessSet 178 | */ 179 | public function shouldFilterForTruthinessByDefault($elements, $expected) 180 | { 181 | $collection = $this->getCollection($elements); 182 | $result = $collection->filter(); 183 | $this->assertEquals($expected, array_values($result->toArray())); 184 | } 185 | 186 | /** 187 | * @test 188 | * @dataProvider truthinessSet 189 | */ 190 | public function shouldBeAbleToFilterByCustomCriteria($elements, $expected) 191 | { 192 | $collection = $this->getCollection($elements); 193 | $result = $collection->filter(function ($v) { 194 | return (bool) $v; 195 | }); 196 | $this->assertEquals($expected, array_values($result->toArray())); 197 | } 198 | 199 | /** 200 | * @test 201 | * @dataProvider falsinessSet 202 | */ 203 | public function shouldBeAbleToFilterNotForFalsinessByDefault($elements, $expected) 204 | { 205 | $collection = $this->getCollection($elements); 206 | 207 | $result = $collection->filterNot(); 208 | 209 | $this->assertEquals($expected, array_values($result->toArray())); 210 | } 211 | 212 | /** 213 | * @return array 214 | */ 215 | public function falsinessSet() 216 | { 217 | return array( 218 | array(array(true, false), array(false)), 219 | array(array(-1, 0, 1), array(0)), 220 | ); 221 | } 222 | 223 | /** 224 | * @test 225 | * @dataProvider filterNotElements 226 | */ 227 | public function shouldBeAbleToFilterNotByCustomCriteria($elements, $expected) 228 | { 229 | $collection = $this->getCollection($elements); 230 | 231 | $result = $collection->filterNot( 232 | function ($value) { 233 | return $value === 2; 234 | } 235 | ); 236 | 237 | $this->assertEquals($expected, array_values($result->toArray())); 238 | } 239 | 240 | /** 241 | * @return array 242 | */ 243 | public function filterNotElements() 244 | { 245 | return array( 246 | array(array(1, 2, 3), array(1, 3)), 247 | ); 248 | } 249 | 250 | public function concatSet() 251 | { 252 | return array( 253 | array(array(), array(), array()), 254 | array(array(1), array(2), array(1, 2)), 255 | array(array('foo' => 1), array('foo' => 2), array(1, 2)) 256 | ); 257 | } 258 | 259 | /** 260 | * @test 261 | * @dataProvider concatSet 262 | */ 263 | public function shouldBeAbleToConcatenate($a, $b, $expected) 264 | { 265 | $left = $this->getCollection($a); 266 | $right = $this->getCollection($b); 267 | $result = $left->concatenate($right); 268 | $this->assertEquals($expected, $result->toArray()); 269 | } 270 | 271 | public function unionSet() 272 | { 273 | return array( 274 | array(array(), array(), array()), 275 | array(array(1), array(2), array(2)), 276 | array(array('foo' => 1), array('foo' => 2), array('foo' => 2)), 277 | array(array('bar' => 2, 'foo' => 1), array('foo' => 2), array('bar' => 2, 'foo' => 2)) 278 | ); 279 | } 280 | 281 | /** 282 | * @test 283 | * @dataProvider unionSet 284 | */ 285 | public function shouldBeAbleToCreateUnionMap($a, $b, $expected) 286 | { 287 | $left = $this->getCollection($a); 288 | $right = $this->getCollection($b); 289 | $result = $left->union($right); 290 | $this->assertEquals($expected, $result->toArray()); 291 | } 292 | 293 | /** 294 | * @test 295 | * @dataProvider mixedElements 296 | */ 297 | public function shouldBeAbleToGetValueCollection($elements) 298 | { 299 | $collection = $this->getCollection($elements); 300 | $result = $collection->values(); 301 | $this->assertEquals(array_values($elements), $result->toArray()); 302 | } 303 | 304 | /** 305 | * @test 306 | * @dataProvider mixedElements 307 | */ 308 | public function shouldBeAbleToGetKeyCollection($elements) 309 | { 310 | $collection = $this->getCollection($elements); 311 | $result = $collection->keys(); 312 | $this->assertEquals(array_keys($elements), $result->toArray()); 313 | } 314 | 315 | public function indexBySet() 316 | { 317 | return array( 318 | array(array(), array()), 319 | array(array('foo'), array('foo' => 'foo')), 320 | array(array('foo' => 'bar'), array('bar' => 'bar')), 321 | array(array('foo' => 'bar', 'foo'), array('bar' => 'bar', 'foo' => 'foo')) 322 | ); 323 | } 324 | 325 | /** 326 | * @test 327 | * @dataProvider indexBySet 328 | */ 329 | public function shouldBeAbleToIndexByCallback($elements, $expected) 330 | { 331 | $collection = $this->getCollection($elements); 332 | $result = $collection->indexBy(function($value) { 333 | return $value; 334 | }); 335 | $this->assertEquals($expected, $result->toArray()); 336 | } 337 | 338 | public function groupBySet() 339 | { 340 | return array( 341 | array(array(), array()), 342 | array(array('foo'), array('foo' => array('foo'))), 343 | array(array('foo', 'foo'), array('foo' => array('foo', 'foo'))), 344 | array(array('foo', 'bar'), array('foo' => array('foo'), 'bar' => array('bar'))), 345 | array(array('foo' => 'bar'), array('bar' => array('bar'))) 346 | ); 347 | } 348 | 349 | /** 350 | * @test 351 | * @dataProvider groupBySet 352 | */ 353 | public function shouldBeAbleToGroupByCallback($elements, $expected) 354 | { 355 | $collection = $this->getCollection($elements); 356 | $collection = $collection->groupBy(function($value) { 357 | return $value; 358 | }); 359 | $result = array(); 360 | foreach ($collection as $key => $value) { 361 | $result[$key] = ($value instanceof Collection) ? $value->toArray() : $value; 362 | } 363 | $this->assertEquals($expected, $result); 364 | } 365 | 366 | public function pickFromArraySet() 367 | { 368 | return array( 369 | array(array(), array()), 370 | array(array(array('foo' => 'bar')), array('bar')), 371 | array(array(array('foo' => 'bar'), array('foo' => 'qux')), array('bar', 'qux')), 372 | array(array(array('bar')), array(null)), 373 | ); 374 | } 375 | 376 | /** 377 | * @test 378 | * @dataProvider pickFromArraySet 379 | */ 380 | public function shouldBeAbleToPickKeysFromArray($elements, $expected) 381 | { 382 | $collection = $this->getCollection($elements); 383 | $result = $collection->pick('foo'); 384 | $this->assertEquals($expected, $result->toArray()); 385 | } 386 | 387 | public function pickFromObjectSet() 388 | { 389 | return array( 390 | array(array(), array()), 391 | array(array((object) array('foo' => 'bar')), array('bar')), 392 | array(array((object) array('foo' => 'bar'), (object) array('foo' => 'qux')), array('bar', 'qux')), 393 | array(array((object) array('bar')), array(null)), 394 | ); 395 | } 396 | 397 | /** 398 | * @test 399 | * @dataProvider pickFromObjectSet 400 | */ 401 | public function shouldBeAbleToPickKeysFromObject($elements, $expected) 402 | { 403 | $collection = $this->getCollection($elements); 404 | $result = $collection->pick('foo'); 405 | $this->assertEquals($expected, $result->toArray()); 406 | } 407 | 408 | /** 409 | * @test 410 | */ 411 | public function shouldBeAbleToMapByInvokingMethodOnElements() 412 | { 413 | $collection = $this->getCollection(array(function() { return 'foo'; })); 414 | $result = $collection->invoke('__invoke'); 415 | $this->assertEquals(array('foo'), $result->toArray()); 416 | } 417 | 418 | public function flattenArraySet() 419 | { 420 | return array( 421 | array(array(), array()), 422 | array(array('foo'), array('foo')), 423 | array(array(array('foo')), array('foo')), 424 | array(array('foo', array('bar')), array('foo', 'bar')) 425 | ); 426 | } 427 | 428 | /** 429 | * @test 430 | * @dataProvider flattenArraySet 431 | */ 432 | public function shouldBeAbleToFlattenArrays($elements, $expected) 433 | { 434 | $collection = $this->getCollection($elements); 435 | $result = $collection->flatten(); 436 | $this->assertEquals($expected, $result->toArray()); 437 | } 438 | 439 | public function flattenTraversableSet() 440 | { 441 | $set = $this->flattenArraySet(); 442 | foreach ($set as $key => $args) { 443 | list($elements, $expected) = $args; 444 | $set[$key] = array($this->traversable($elements), $expected); 445 | } 446 | return $set; 447 | } 448 | 449 | /** 450 | * @return \ArrayIterator 451 | */ 452 | private function traversable(array $items) 453 | { 454 | foreach ($items as $key => $value) { 455 | if (is_array($value)) { 456 | $items[$key] = new \ArrayObject($this->traversable($value)); 457 | } else { 458 | $items[$key] = $value; 459 | } 460 | } 461 | return $items; 462 | } 463 | 464 | /** 465 | * @test 466 | * @dataProvider flattenTraversableSet 467 | */ 468 | public function shouldBeAbleToFlattenTraversables($elements, $expected) 469 | { 470 | $collection = $this->getCollection($elements); 471 | $result = $collection->flatten(); 472 | $this->assertEquals($expected, $result->toArray()); 473 | } 474 | 475 | /** 476 | * @return array 477 | */ 478 | public function strictlyUniqueSet() 479 | { 480 | return array( 481 | array(array(), array()), 482 | array(array('foo', 'bar'), array('foo', 'bar')), 483 | array(array('foo', 'foo'), array('foo')), 484 | array(array($a = new \stdClass, $b = new \stdClass), array($a, $b)), 485 | array(array($o = new \stdClass, $o), array($o)) 486 | ); 487 | } 488 | 489 | /** 490 | * @test 491 | * @dataProvider strictlyUniqueSet 492 | */ 493 | public function shouldBeAbleToFilterUniquesStrictly($elements, $expected) 494 | { 495 | $collection = $this->getCollection($elements); 496 | $result = $collection->unique(); 497 | $this->assertEquals($expected, $result->toArray()); 498 | } 499 | 500 | /** 501 | * @return array 502 | */ 503 | public function nonStrictlyUniqueSet() 504 | { 505 | return array( 506 | array(array(), array()), 507 | array(array('foo', 'bar'), array('foo', 'bar')), 508 | array(array('foo', 'foo'), array('foo')), 509 | array(array('100', 100), array('100')), 510 | array(array(100, '100'), array(100)) 511 | ); 512 | } 513 | 514 | /** 515 | * @test 516 | * @dataProvider nonStrictlyUniqueSet 517 | */ 518 | public function shouldBeAbleToFilterUniquesNonStrictly($elements, $expected) 519 | { 520 | // FIXME: The boolean parameter implies a code smell 521 | $collection = $this->getCollection($elements); 522 | $result = $collection->unique(false); 523 | $this->assertEquals($expected, $result->toArray()); 524 | } 525 | 526 | /** 527 | * @return array 528 | */ 529 | public function sortWithSet() 530 | { 531 | return array( 532 | array(array(), array()), 533 | array(array(1), array(1)), 534 | array(array(2, 1), array(1, 2)) 535 | ); 536 | } 537 | 538 | /** 539 | * @test 540 | * @dataProvider sortWithSet 541 | */ 542 | public function shouldBeAbleToSortWithComparator($elements, $expected) 543 | { 544 | $collection = $this->getCollection($elements); 545 | $result = $collection->sortWith(function($a, $b) { 546 | return $a - $b; 547 | }); 548 | $this->assertEquals($expected, $result->toArray()); 549 | } 550 | 551 | public function sortBySet() 552 | { 553 | return array( 554 | array(array(), array()), 555 | array(array('foo'), array('foo')), 556 | array(array('foo', 'quxen', 'qux'), array('foo', 'qux', 'quxen')) 557 | ); 558 | } 559 | 560 | /** 561 | * @test 562 | * @dataProvider sortBySet 563 | */ 564 | public function shouldBeAbleToSortByMetric($elements, $expected) 565 | { 566 | $collection = $this->getCollection($elements); 567 | $result = $collection->sortBy(function($v) { 568 | return strlen($v); 569 | }); 570 | $this->assertEquals($expected, $result->toArray()); 571 | } 572 | 573 | /** 574 | * @test 575 | * @dataProvider addValue 576 | * 577 | * @param array $elements 578 | * @param mixed $value 579 | * @param array $expected 580 | */ 581 | public function shouldBeAbleToAddValue(array $elements, $value, array $expected) 582 | { 583 | $collection = $this->getCollection($elements); 584 | $result = $collection->add($value); 585 | 586 | $this->assertEquals($expected, $result->toArray()); 587 | } 588 | 589 | /** 590 | * @return array 591 | */ 592 | public function addValue() 593 | { 594 | return array( 595 | array(array('a', 'b'), 'c', array('a', 'b', 'c')), 596 | array(array(1, 3), 2, array(1, 3, 2)), 597 | ); 598 | } 599 | 600 | /** 601 | * @test 602 | * @dataProvider addKeyAndValue 603 | * 604 | * @param array $elements 605 | * @param mixed $key 606 | * @param mixed $value 607 | * @param array $expected 608 | */ 609 | public function shouldBeAbleToAddKeyAndValue(array $elements, $key, $value, array $expected) 610 | { 611 | $collection = $this->getCollection($elements); 612 | $result = $collection->add($value, $key); 613 | 614 | $this->assertEquals($expected, $result->toArray()); 615 | } 616 | 617 | /** 618 | * @return array 619 | */ 620 | public function addKeyAndValue() 621 | { 622 | return array( 623 | array(array('a' => 'b'), 'c', 'd', array('a' => 'b', 'c' => 'd')), 624 | array(array(1 => 'a', 3 => 'b'), 2, 'c', array(1 => 'a', 3 => 'b', 2 => 'c')), 625 | ); 626 | } 627 | 628 | /** 629 | * @test 630 | * @dataProvider minimumProvider 631 | */ 632 | public function shouldBeAbleToGetMinimumOfValues($elements, $expected) 633 | { 634 | $collection = $this->getCollection($elements); 635 | 636 | $this->assertEquals($expected, $collection->min()); 637 | } 638 | 639 | /** 640 | * @return array 641 | */ 642 | public function minimumProvider() 643 | { 644 | return array( 645 | array(array(1, 2, 3), 1), 646 | array(array(5, 2), 2), 647 | ); 648 | } 649 | 650 | /** 651 | * @test 652 | */ 653 | public function minimumOfValuesOnAnEmptyCollectionShouldThrowAnException() 654 | { 655 | $collection = $this->getCollection(); 656 | 657 | $this->setExpectedException( 658 | 'UnderflowException', 659 | 'Can not get a minimum value on an empty collection.' 660 | ); 661 | 662 | $collection->min(); 663 | } 664 | 665 | /** 666 | * @test 667 | * @dataProvider maximumProvider 668 | */ 669 | public function shouldBeAbleToGetMaximumOfValues($elements, $expected) 670 | { 671 | $collection = $this->getCollection($elements); 672 | 673 | $this->assertEquals($expected, $collection->max()); 674 | } 675 | 676 | /** 677 | * @return array 678 | */ 679 | public function maximumProvider() 680 | { 681 | return array( 682 | array(array(1, 2, 3), 3), 683 | array(array(5, 2), 5), 684 | ); 685 | } 686 | 687 | /** 688 | * @test 689 | */ 690 | public function maximumOfValuesOnAnEmptyCollectionShouldThrowAnException() 691 | { 692 | $collection = $this->getCollection(); 693 | 694 | $this->setExpectedException( 695 | 'UnderflowException', 696 | 'Can not get a maximum value on an empty collection.' 697 | ); 698 | 699 | $collection->max(); 700 | } 701 | 702 | /** 703 | * @test 704 | * @dataProvider sumProvider 705 | */ 706 | public function shouldBeAbleToGetSumOfValues($elements, $expected) 707 | { 708 | $collection = $this->getCollection($elements); 709 | 710 | $this->assertEquals($expected, $collection->sum()); 711 | } 712 | 713 | /** 714 | * @return array 715 | */ 716 | public function sumProvider() 717 | { 718 | return array( 719 | array(array(), 0), 720 | array(array(0), 0), 721 | array(array(1, 2), 3), 722 | array(array(2, 4, 6), 12), 723 | ); 724 | } 725 | 726 | /** 727 | * @test 728 | * @dataProvider productProvider 729 | */ 730 | public function shouldBeAbleToGetProductOfValues($elements, $expected) 731 | { 732 | $collection = $this->getCollection($elements); 733 | 734 | $this->assertEquals($expected, $collection->product()); 735 | } 736 | 737 | /** 738 | * @return array 739 | */ 740 | public function productProvider() 741 | { 742 | return array( 743 | array(array(), 1), 744 | array(array(0), 0), 745 | array(array(1, 2), 2), 746 | array(array(2, 3, 5), 30), 747 | ); 748 | } 749 | 750 | /** 751 | * @test 752 | * @dataProvider restElements 753 | */ 754 | public function shouldBeAbleToTakeRestOfTheElementsExceptFirst($elements, $expected) 755 | { 756 | $collection = $this->getCollection($elements); 757 | 758 | $result = $collection->rest(); 759 | 760 | $this->assertEquals($expected, array_values($result->toArray())); 761 | } 762 | 763 | /** 764 | * @return array 765 | */ 766 | public function restElements() 767 | { 768 | return array( 769 | array(array(), array()), 770 | array(array('a'), array()), 771 | array(array('a', 'b', 'c'), array('b', 'c')), 772 | ); 773 | } 774 | 775 | /** 776 | * @test 777 | * @dataProvider restWithIndexedElements 778 | */ 779 | public function takingRestOfElementsShouldMaintainIndexAssociations($elements, $expected) 780 | { 781 | $collection = $this->getCollection($elements); 782 | 783 | $result = $collection->rest(); 784 | 785 | $this->assertEquals($expected, $result->toArray()); 786 | } 787 | 788 | /** 789 | * @return array 790 | */ 791 | public function restWithIndexedElements() 792 | { 793 | return array( 794 | array(array(1 => 'a', 2 => 'b', 'c' => 'd'), array(2 => 'b', 'c' => 'd')), 795 | array(array(0 => 'a', 1 => 'b', '2' => 'c', 3 => 'd'), array(1 => 'b', '2' => 'c', 3 => 'd')), 796 | ); 797 | } 798 | 799 | /** 800 | * @test 801 | * @dataProvider partitionElements 802 | */ 803 | public function shouldBeAbleToPartitionElements($elements, $first, $second) 804 | { 805 | $collection = $this->getCollection($elements); 806 | 807 | $result = $collection->partition( 808 | function ($value) { 809 | return $value < 3; 810 | } 811 | ); 812 | 813 | $this->assertEquals($first, array_values($result->first()->toArray())); 814 | $this->assertEquals($second, array_values($result->last()->toArray())); 815 | } 816 | 817 | /** 818 | * @return array 819 | */ 820 | public function partitionElements() 821 | { 822 | return array( 823 | array(array(), array(), array()), 824 | array(array(1, 2, 3), array(1, 2), array(3)), 825 | ); 826 | } 827 | 828 | /** 829 | * @test 830 | * @dataProvider partitionWithIndexedElements 831 | */ 832 | public function partitionShouldMaintainIndexAssociations($elements, $first, $second) 833 | { 834 | $collection = $this->getCollection($elements); 835 | 836 | $result = $collection->partition( 837 | function ($value) { 838 | return $value < 3; 839 | } 840 | ); 841 | 842 | $this->assertEquals($first, $result->first()->toArray()); 843 | $this->assertEquals($second, $result->last()->toArray()); 844 | } 845 | 846 | /** 847 | * @return array 848 | */ 849 | public function partitionWithIndexedElements() 850 | { 851 | return array( 852 | array(array(2 => 2, 3 => 3), array(2 => 2), array(3 => 3)), 853 | ); 854 | } 855 | 856 | /** 857 | * @test 858 | */ 859 | public function shouldBeAbleToCheckForEmpty() 860 | { 861 | $collection = $this->getCollection(); 862 | 863 | $this->assertTrue($collection->isEmpty()); 864 | } 865 | 866 | /** 867 | * @test 868 | */ 869 | public function shouldBeAbleToCheckForNotEmpty() 870 | { 871 | $collection = $this->getCollection(array('a')); 872 | 873 | $this->assertFalse($collection->isEmpty()); 874 | } 875 | } 876 | -------------------------------------------------------------------------------- /tests/Xi/Collections/Collection/ArrayCollectionTest.php: -------------------------------------------------------------------------------- 1 | view(); 9 | } 10 | } -------------------------------------------------------------------------------- /tests/Xi/Collections/Collection/OuterCollectionTest.php: -------------------------------------------------------------------------------- 1 | view(); 9 | } 10 | } -------------------------------------------------------------------------------- /tests/Xi/Collections/Collection/SimpleCollectionTest.php: -------------------------------------------------------------------------------- 1 | assertFalse($this->getCollection()->force() instanceof SimpleCollectionView); 17 | } 18 | } -------------------------------------------------------------------------------- /tests/Xi/Collections/Enumerable/AbstractEnumerableTest.php: -------------------------------------------------------------------------------- 1 | getEnumerable(); 19 | $enum->tap(function($v) use(&$result) { 20 | $result = $v; 21 | }); 22 | $this->assertSame($enum, $result); 23 | } 24 | 25 | public function mixedElements() 26 | { 27 | return array( 28 | array(array()), 29 | array(array('foo')), 30 | array(array('foo' => 'bar', 'bar' => 'foo', 1, 2, 3)) 31 | ); 32 | } 33 | 34 | /** 35 | * @test 36 | * @dataProvider mixedElements 37 | */ 38 | public function shouldBeAbleToCountElements($values) 39 | { 40 | $enum = $this->getEnumerable($values); 41 | $result = count($enum); 42 | $this->assertEquals(count($values), $result); 43 | } 44 | 45 | /** 46 | * @test 47 | * @dataProvider mixedElements 48 | */ 49 | public function shouldBeAbleToReconstructValuesWithTraversal($values) 50 | { 51 | $result = array(); 52 | $enum = $this->getEnumerable($values); 53 | foreach ($enum as $key => $value) { 54 | $result[$key] = $value; 55 | } 56 | $this->assertEquals($values, $result); 57 | } 58 | 59 | 60 | /** 61 | * @test 62 | * @dataProvider mixedElements 63 | */ 64 | public function shouldBeAbleToReconstructValuesWithEach($values) 65 | { 66 | $result = new \ArrayObject; 67 | $enum = $this->getEnumerable($values); 68 | $enum->each(function($v, $k) use($result) { 69 | $result[$k] = $v; 70 | }); 71 | $this->assertEquals($values, $result->getArrayCopy()); 72 | } 73 | 74 | /** 75 | * @test 76 | * @dataProvider mixedElements 77 | */ 78 | public function shouldBeAbleToReconstructValuesWithReduce($values) 79 | { 80 | $enum = $this->getEnumerable($values); 81 | $result = $enum->reduce(function($result, $value, $key) { 82 | $result[$key] = $value; 83 | return $result; 84 | }, array()); 85 | $this->assertEquals($result, $values); 86 | } 87 | 88 | public function integerElements() 89 | { 90 | return array( 91 | array(array()), 92 | array(array(1)), 93 | array(array(1, 2, 3, 4)) 94 | ); 95 | } 96 | 97 | /** 98 | * @test 99 | * @dataProvider integerElements 100 | */ 101 | public function shouldBeAbleToSumIntegersWithReduce($values) 102 | { 103 | $enum = $this->getEnumerable($values); 104 | $result = $enum->reduce(function($result, $value) { 105 | return $result + $value; 106 | }, 0); 107 | $this->assertEquals($result, array_sum($values)); 108 | } 109 | 110 | public function integerHaystack() 111 | { 112 | return array( 113 | array(array(), null), 114 | array(array(null, '', new \stdClass()), null), 115 | array(array(1), 1), 116 | array(array(1, 2), 1), 117 | array(array(null, '', new \stdClass(), 1), 1) 118 | ); 119 | } 120 | 121 | /** 122 | * @test 123 | * @dataProvider integerHaystack 124 | */ 125 | public function shouldBeAbleToFindMatchingValue($values, $expect) 126 | { 127 | $enum = $this->getEnumerable($values); 128 | $result = $enum->find('is_integer'); 129 | $this->assertEquals($expect, $result); 130 | } 131 | 132 | /** 133 | * @test 134 | * @dataProvider integerHaystack 135 | */ 136 | public function shouldBeAbleToCheckForExistenceOfMatchingValue($values, $expect) 137 | { 138 | $enum = $this->getEnumerable($values); 139 | $result = $enum->exists('is_integer'); 140 | $this->assertEquals(!empty($expect), $result); 141 | } 142 | 143 | public function integerSets() 144 | { 145 | return array( 146 | array(array(), 0), 147 | array(array(1), 1), 148 | array(array(1, 2, 3), 3), 149 | array(array('nope'), 0), 150 | array(array(1, 2, 3, 'nope'), 3) 151 | ); 152 | } 153 | 154 | /** 155 | * @test 156 | * @dataProvider integerSets 157 | */ 158 | public function shouldBeAbleToAssertPredicateForAllValues($values, $integers) 159 | { 160 | $enum = $this->getEnumerable($values); 161 | $result = $enum->forAll('is_integer'); 162 | $this->assertEquals(count($values) == $integers, $result); 163 | } 164 | 165 | /** 166 | * @test 167 | * @dataProvider integerSets 168 | */ 169 | public function shouldBeAbleToCountValuesMatchingPredicate($values, $integers) 170 | { 171 | $enum = $this->getEnumerable($values); 172 | $result = $enum->countAll('is_integer'); 173 | $this->assertEquals($integers, $result); 174 | } 175 | 176 | public function firstValues() 177 | { 178 | return array( 179 | array(array(), null), 180 | array(array(1), 1), 181 | array(array(1, 2), 1) 182 | ); 183 | } 184 | 185 | /** 186 | * @test 187 | * @dataProvider firstValues 188 | */ 189 | public function shouldBeAbleToRetrieveFirstValue($values, $first) 190 | { 191 | $enum = $this->getEnumerable($values); 192 | $result = $enum->first(); 193 | $this->assertEquals($first, $result); 194 | } 195 | 196 | public function lastValues() 197 | { 198 | return array( 199 | array(array(), null), 200 | array(array(1), 1), 201 | array(array(1, 2), 2) 202 | ); 203 | } 204 | 205 | /** 206 | * @test 207 | * @dataProvider lastValues 208 | */ 209 | public function shouldBeAbleToRetrieveLastValue($values, $last) 210 | { 211 | $enum = $this->getEnumerable($values); 212 | $result = $enum->last(); 213 | $this->assertEquals($last, $result); 214 | } 215 | } -------------------------------------------------------------------------------- /tests/Xi/Collections/Enumerable/ArrayEnumerableTest.php: -------------------------------------------------------------------------------- 1 | assertSame($inner, $outer->getInnerEnumerable()); 19 | } 20 | } -------------------------------------------------------------------------------- /tests/Xi/Collections/Enumerable/SimpleEnumerableTest.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./Xi 6 | 7 | 8 | 9 | --------------------------------------------------------------------------------