src/Lasso3000/Enumerable/Enumerable.php
<?php
/**
* lasso3000/enumerable - A implementation of ruby's Enumerable module written in PHP.
* Copyright (C) 2016-2017 Lars Olsson <lasso@lassoweb.se>
*
* This file is part of lasso3000/enumerable.
*
* lasso3000/enumerable is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* lasso3000/enumerable is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with lasso3000/enumerable. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Lasso3000\Enumerable;
/**
* The Enumerable trait will allow any object to work as a collection. The using class must
* implement the __each method, all other methods in this trait only depend on that method.
*/
trait Enumerable
{
/**
* Returns one element at a time from the enumerable. When implementing this method,
* you should only return values using the yield keyword. Do not use the return keyword
* to return values from this method!
*/
protected abstract function __each();
/**
* Returns true if all elements in the enumerable returns true from the callback.
* Otherwise returns false.
*
* @param callable $callback
* @return boolean
*/
public function all(callable $callback)
{
foreach ($this->__each() as $elem) {
if ($callback($elem) === false) {
return false;
}
}
return true;
}
/**
* Returns true if any elements in the enumerable returns true from the callback.
* Otherwise returns false.
*
* @param callable $callback
* @return boolean
*/
public function any(callable $callback)
{
foreach ($this->__each() as $elem) {
if ($callback($elem) === true) {
return true;
}
}
return false;
}
/**
* Returns the number of elements in the enumerable. If a callback is provided,
* only those alements that return true from the callback are counted.
* Otherwise returns false.
*
* @param callable $callback
* @return int
*/
public function count(callable $callback = null)
{
$numElems = 0;
if ($callback) {
foreach ($this->__each() as $elem) {
if ($callback($elem)) {
$numElems++;
}
}
return $numElems;
}
foreach ($this->__each() as $elem) {
$numElems++;
}
return $numElems;
}
/**
* Drops a number of elemens from the enumerable and returns the remaining
* elements as an EnumerableArray.
*
* @param int $numElems
* @return EnumerableArray
* @throws \InvalidArgumentException
*/
public function drop($numElems)
{
if (!is_int($numElems) || $numElems < 0) {
throw new \InvalidArgumentException("Argument must be a positive integer or 0.");
}
$elems = new EnumerableArray();
$numDropped = 0;
foreach ($this->__each() as $elem) {
if ($numDropped++ < $numElems) {
continue;
}
$elems->append($elem);
}
return $elems;
}
/**
* Drops elements until the provided callback returns true. The remaining elements
* are returned as an EnumerableArray.
*
* @param callable $callback
* @return EnumerableArray
*/
public function dropWhile(callable $callback)
{
$elems = new EnumerableArray();
$stillDropping = true;
foreach ($this->__each() as $elem) {
if ($stillDropping) {
if ($callback($elem) === true) {
continue;
}
$stillDropping = false;
}
$elems->append($elem);
}
return $elems;
}
/**
* Applies a callback on each element in the enumerable without returning any value.
*
* @param callable $callback
* @return null
*/
public function each(callable $callback)
{
foreach ($this->__each() as $elem) {
$callback($elem);
}
}
/**
* Applies callback (like map) to each slice of the enumerable and returns the results
* as an EnumerableArray.
*
* @param int $sliceSize
* @param callable $callback
* @return EnumerableArray
* @throws \InvalidArgumentException
*/
public function eachSlice($sliceSize, callable $callback)
{
if (!is_int($sliceSize) || $sliceSize < 1) {
throw new \InvalidArgumentException("Argument must be a positive integer.");
}
$idx = 0;
$slices = new EnumerableArray();
$currentSlice = new EnumerableArray();
foreach ($this->__each() as $elem) {
$currentSlice->append($elem);
if (++$idx % $sliceSize === 0) {
$slices->append($currentSlice);
$currentSlice = new EnumerableArray();
}
}
$slices->append($currentSlice);
return $slices->map($callback);
}
/**
* Returns the first element in the enumerable that returns true from the callback.
* If no matching element can be found, the provided default is returned instead.
*
* @param callable $callback
* @param mixed $default
* @return mixed
*/
public function find(callable $callback, $default = null)
{
foreach ($this->__each() as $elem) {
if ($callback($elem) === true) {
return $elem;
}
}
return $default;
}
/**
* Returns all elements in the enumerable that returns true from the callback.
*
* @param callable $callback
* @return EnumerableArray
*/
public function findAll(callable $callback)
{
$elems = new EnumerableArray();
foreach ($this->__each() as $elem) {
if ($callback($elem) === true) {
$elems->append($elem);
}
}
return $elems;
}
/**
* Returns the index where $needle can be found. If $needle cannot be found, null is returned.
* $needle can be either a callable or a value.
*
* @param mixed $needle
* @return int|null
*/
public function findIndex($needle)
{
if (is_callable($needle)) {
foreach ($this->__each() as $idx => $elem) {
if ($needle($elem) === true) {
return $idx;
}
}
return null;
}
foreach ($this->__each() as $idx => $elem) {
if ($needle === $elem) {
return $idx;
}
}
return null;
}
/**
* Applies callback to all elements in the enumerable and returns an EnumerableArray
* with new result.
*
* @param callable $callback
* @return EnumerableArray
*/
public function map(callable $callback)
{
$elems = new EnumerableArray();
foreach ($this->__each() as $elem) {
$elems->append($callback($elem));
}
return $elems;
}
/**
* Returns true if the provided argument is a member of the enumerable.
* Otherwise returns false.
*
* @param mixed $needle
* @return boolean
*/
public function member($needle)
{
foreach ($this->__each() as $elem) {
if ($needle === $elem) {
return true;
}
}
return false;
}
/**
* Returns true if no element in the enumerable returns true from the callback.
*
* @param callable $callback
* @return boolean
*/
public function none(callable $callback)
{
foreach ($this->__each() as $elem) {
if ($callback($elem) === true) {
return false;
}
}
return true;
}
/**
* Returns true if exactly one element in the enumerable returns true from the callback.
*
* @param callable $callback
* @return boolean
*/
public function one(callable $callback)
{
$numMatchingElems = 0;
foreach ($this->__each() as $elem) {
if ($callback($elem) === true) {
if (++$numMatchingElems > 1) {
return false;
}
}
}
return $numMatchingElems === 1;
}
/**
* Returns an EnumerableArray with two elements, one EnumerableArray with all elements
* in the enumerable that returns true from the callback, and one EnumerableArray with
* all elements in the enumerable that returns false from the callback.
*
* @param callable $callback
* @return EnumerableArray
*/
public function partition(callable $callback)
{
$matching = new EnumerableArray();
$notMatching = new EnumerableArray();
foreach ($this->__each() as $elem) {
$receiver = ($callback($elem) === true ? $matching : $notMatching);
$receiver->append($elem);
}
return new EnumerableArray([$matching, $notMatching]);
}
/**
* Reduces an enumerable into a single value by applying a callback to each element.
*
* @param callable $callback
* @param mixed $initialValue
* @return EnumerableArray
*/
public function reduce(callable $callback, $initialValue = null)
{
$memo = $initialValue;
foreach ($this->__each() as $elem) {
$memo = $callback($memo, $elem);
}
return $memo;
}
/**
* Returns an EnumerableArray containing all elements in the enumerable that returns
* false from the callback.
*
* @param callable $callback
* @return EnumerableArray
*/
public function reject(callable $callback)
{
$elems = new EnumerableArray();
foreach ($this->__each() as $elem) {
if ($callback($elem) === false) {
$elems->append($elem);
}
}
return $elems;
}
/**
* Applies a callback on each element in the enumerable in reverse order without
* returning any value.
*
* @param callable $callback
* @return null
*/
public function reverseEach(callable $callback)
{
$elems = [];
foreach ($this->__each() as $elem) {
array_unshift($elems, $elem);
}
foreach ($elems as $elem) {
$callback($elem);
}
}
/**
* Sorts the elements in the enumerable by using a callback.
*
* @param callable $callback
* @return EnumerableArray
*/
public function sort(callable $callback)
{
$elems = [];
foreach ($this->__each() as $elem) {
$elems[] = $elem;
}
usort($elems, $callback);
return new EnumerableArray($elems);
}
/**
* Takes a number of elemens from the enumerable and returns them as an EnumerableArray.
*
* @param int $numElems
* @return EnumerableArray
* @throws \InvalidArgumentException
*/
public function take($numElems)
{
if (!is_int($numElems) || $numElems < 0) {
throw new \InvalidArgumentException("Argument must be a positive integer or 0.");
}
$elems = new EnumerableArray();
$numTaken = 0;
foreach ($this->__each() as $elem) {
if ($numTaken++ == $numElems) {
break;
}
$elems->append($elem);
}
return $elems;
}
/**
* Takes elements from the enumerable until the provided callback returns false.
* The result is returned as an EnumerableArray.
*
* @param callable $callback
* @return EnumerableArray
*/
public function takeWhile(callable $callback)
{
$elems = new EnumerableArray();
foreach ($this->__each() as $elem) {
if ($callback($elem) === false) {
return $elems;
}
$elems->append($elem);
}
return $elems;
}
}