Skip to content

Commit

Permalink
Add initial support for generators
Browse files Browse the repository at this point in the history
  • Loading branch information
mpetrovich committed Aug 19, 2018
1 parent 9e73aa8 commit 400de52
Show file tree
Hide file tree
Showing 13 changed files with 675 additions and 30 deletions.
20 changes: 19 additions & 1 deletion README.md
Expand Up @@ -119,7 +119,7 @@ At a glance

Highlights
---
- [Many data types supported](#supported-data-types): arrays, objects, [`Traversable`](http://php.net/manual/en/class.traversable.php), [`DirectoryIterator`](http://php.net/manual/en/class.directoryiterator.php), and more
- [Many data types supported](#supported-data-types): arrays, objects, [generators](http://php.net/manual/en/language.generators.overview.php), [`Traversable`](http://php.net/manual/en/class.traversable.php), [`DirectoryIterator`](http://php.net/manual/en/class.directoryiterator.php), and more
- [Chaining](#chaining)
- [Currying](#currying)
- [Lazy evaluation](#lazy-evaluation)
Expand Down Expand Up @@ -254,6 +254,7 @@ $chain->run();
Dash can work with a wide variety of data types, including:
- arrays
- objects (eg. `stdClass`)
- [generators](http://php.net/manual/en/language.generators.overview.php)
- anything that implements the [`Traversable`](http://php.net/manual/en/class.traversable.php) interface
- [`DirectoryIterator`](http://php.net/manual/en/class.directoryiterator.php), which is also a `Traversable` but cannot normally be used with `iterator_to_array()` [due to a PHP bug](https://bugs.php.net/bug.php?id=49755). Dash works around this transparently.

Expand Down Expand Up @@ -292,6 +293,23 @@ Dash\chain(new ArrayObject(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]))
// === 5
```

With a generator:

```php
$integers = function () {
for ($int = 1; true; $int++) {
yield $int;
}
};

Dash\chain($integers())
->filter('Dash\isOdd')
->take(3)
->reverse()
->value();
// === [5, 3, 1]
```

With a `DirectoryIterator`:

```php
Expand Down
6 changes: 4 additions & 2 deletions composer.json
Expand Up @@ -11,7 +11,7 @@
}
],
"require": {
"php": ">=5.4.0"
"php": ">=5.5.0"
},
"autoload": {
"psr-4": {
Expand Down Expand Up @@ -167,7 +167,9 @@
"src/Curry/toArray.php",
"src/Curry/toObject.php",
"src/Curry/unary.php",
"src/Curry/values.php"
"src/Curry/values.php",
"src/Generator/filter.php",
"src/Generator/take.php"
]
},
"require-dev": {
Expand Down
12 changes: 6 additions & 6 deletions docs/Operations.md
Expand Up @@ -14,7 +14,7 @@ Operation | Signature
[deltas](#deltas) | `deltas($iterable): array`
[difference](#difference) | `difference($iterable /*, ...iterables */): array`
[each](#each) | `each($iterable, $iteratee): mixed`
[filter](#filter) | `filter($iterable, $predicate = 'Dash\identity'): array`
[filter](#filter) | `filter($iterable, $predicate = 'Dash\identity'): array\|iterable`
[find](#find) | `find($iterable, $predicate = 'Dash\identity'): array\|null`
[findKey](#findkey) | `findKey($iterable, $predicate = 'Dash\identity'): string\|null`
[findLast](#findlast) | `findLast($iterable, $predicate = 'Dash\identity'): array\|null`
Expand Down Expand Up @@ -45,7 +45,7 @@ Operation | Signature
[rotate](#rotate) | `rotate($iterable, $count = 1): array`
[sort](#sort) | `sort($iterable, $comparator = 'Dash\compare'): array`
[sum](#sum) | `sum($iterable): numeric`
[take](#take) | `take($iterable, $count = 1): array`
[take](#take) | `take($iterable, $count = 1): array\|iterable`
[takeRight](#takeright) | `takeRight($iterable, $count = 1): array`
[toArray](#toarray) | `toArray($value): array`
[toObject](#toobject) | `toObject($value): object`
Expand Down Expand Up @@ -434,7 +434,7 @@ filter
[Operations](#operations)[Iterable](#iterable)

```php
filter($iterable, $predicate = 'Dash\identity'): array
filter($iterable, $predicate = 'Dash\identity'): array|iterable

# Curried: (all parameters required)
Curry\filter($predicate, $iterable)
Expand All @@ -450,7 +450,7 @@ Parameter | Type | Description
--- | --- | :---
`$iterable` | `iterable\|stdClass\|null` |
`$predicate` | `callable\|string\|array` | (optional) If a callable, invoked with `($value, $key, $iterable)` for each element in `$iterable`; if a string, will get elements with truthy values at `$field`; if an array of form `[$field, $value]`, will get elements whose `$field` loosely equals `$value`
**Returns** | `array` | List of elements in `$iterable` that satisfy `$predicate`
**Returns** | `array\|iterable` | List of elements in `$iterable` that satisfy `$predicate`

**Example:**
```php
Expand Down Expand Up @@ -1840,7 +1840,7 @@ take
[Operations](#operations)[Iterable](#iterable)

```php
take($iterable, $count = 1): array
take($iterable, $count = 1): array|iterable

# Curried: (all parameters required)
Curry\take($count, $iterable)
Expand All @@ -1856,7 +1856,7 @@ Parameter | Type | Description
--- | --- | :---
`$iterable` | `iterable\|stdClass\|null` |
`$count` | `integer` | If negative, gets all but the last `$count` elements of `$iterable`
**Returns** | `array` | New array of `$count` elements
**Returns** | `array\|iterable` | New array of `$count` elements

**Example:**
```php
Expand Down
32 changes: 32 additions & 0 deletions src/Generator/filter.php
@@ -0,0 +1,32 @@
<?php

namespace Dash\Generator;

/**
* @see Dash\filter()
*/
// @codingStandardsIgnoreLine
function filter($iterable, $predicate = 'Dash\identity')
{
if (!is_callable($predicate)) {
$predicate = call_user_func_array('Dash\matchesProperty', (array) $predicate);
}

$index = 0;
$isIndexedArray = true;

foreach ($iterable as $key => $value) {
$isIndexedArray = $isIndexedArray && ($key === $index);

if (call_user_func($predicate, $value, $key, $iterable)) {
if ($isIndexedArray) {
yield $value;
}
else {
yield $key => $value;
}
}

$index++;
}
}
18 changes: 18 additions & 0 deletions src/Generator/take.php
@@ -0,0 +1,18 @@
<?php

namespace Dash\Generator;

/**
* @see Dash\take()
*/
// @codingStandardsIgnoreLine
function take($iterable, $count = 1)
{
foreach ($iterable as $key => $value) {
if ($count < 1) {
break;
}
yield $key => $value;
$count--;
}
}
10 changes: 3 additions & 7 deletions src/_.php
Expand Up @@ -158,8 +158,7 @@ public static function unsetCustom($name)
*/
public static function __callStatic($method, $args)
{
$callable = self::toCallable($method);
return call_user_func_array($callable, $args);
return call_user_func_array(self::toCallable($method), $args);
}

/**
Expand Down Expand Up @@ -352,12 +351,10 @@ private static function toCallable($method)
if (is_callable("\\Dash\\$method")) {
return "\\Dash\\$method";
}
elseif (isset(self::$customFunctions[$method])) {
if (isset(self::$customFunctions[$method])) {
return self::$customFunctions[$method];
}
else {
throw new \BadMethodCallException("No operation named '$method' found");
}
throw new \BadMethodCallException("No operation named '$method' found");
}

/**
Expand All @@ -381,7 +378,6 @@ private function __construct($input = null)
private function toOperation($method, $args)
{
$callable = self::toCallable($method);

$operation = function ($input) use ($callable, $args) {
array_unshift($args, $input);
return call_user_func_array($callable, $args);
Expand Down
8 changes: 6 additions & 2 deletions src/filter.php
Expand Up @@ -17,7 +17,7 @@
* if a string, will get elements with truthy values at `$field`;
* if an array of form `[$field, $value]`, will get elements
* whose `$field` loosely equals `$value`
* @return array List of elements in `$iterable` that satisfy `$predicate`
* @return array|iterable List of elements in `$iterable` that satisfy `$predicate`
*
* @example
Dash\filter([1, 2, 3, 4], 'Dash\isEven');
Expand Down Expand Up @@ -53,7 +53,11 @@ function ($value, $key) { return $key > 1; }
*/
function filter($iterable, $predicate = 'Dash\identity')
{
assertType($iterable, ['iterable', 'stdClass', 'null'], __FUNCTION__);
assertType($iterable, ['Generator', 'iterable', 'stdClass', 'null'], __FUNCTION__);

if ($iterable instanceof \Generator) {
return Generator\filter($iterable, $predicate);
}

if (is_null($iterable)) {
return [];
Expand Down
11 changes: 9 additions & 2 deletions src/take.php
Expand Up @@ -13,7 +13,7 @@
* @category Iterable
* @param iterable|stdClass|null $iterable
* @param integer $count If negative, gets all but the last `$count` elements of `$iterable`
* @return array New array of `$count` elements
* @return array|iterable New array of `$count` elements
*
* @example
Dash\take([2, 3, 5, 8, 13], 3);
Expand All @@ -27,7 +27,14 @@
*/
function take($iterable, $count = 1)
{
assertType($iterable, ['iterable', 'stdClass', 'null'], __FUNCTION__);
assertType($iterable, ['Generator', 'iterable', 'stdClass', 'null'], __FUNCTION__);

if ($iterable instanceof \Generator) {
if ($count < 0) {
throw new \InvalidArgumentException('Count cannot be negative when using a generator');
}
return Generator\take($iterable, $count);
}

$array = toArray($iterable);
$preserveKeys = !isIndexedArray($array);
Expand Down
60 changes: 60 additions & 0 deletions tests/GeneratorTest.php
@@ -0,0 +1,60 @@
<?php

class GeneratorTest extends PHPUnit_Framework_TestCase
{
/**
* @dataProvider cases
*/
public function testComposing($generator)
{
$result = Dash\join(
Dash\map(
Dash\take(
Dash\filter(
$generator(),
'Dash\isEven'
),
3
)
),
', '
);
$this->assertSame('2, 4, 6', $result);
}

/**
* @dataProvider cases
*/
public function testChaining($generator)
{
$result = Dash\chain($generator())
->filter('Dash\isEven')
->take(3)
->join(', ')
->value();

$this->assertSame('2, 4, 6', $result);
}

public function cases()
{
return [
'With a finite generator' => [
function () {
foreach (range(1, 10) as $key => $value) {
yield $key => $value;
}
},
],
'With an infinite generator' => [
function () {
$current = 1;
while (true) {
yield $current;
$current++;
}
},
],
];
}
}
32 changes: 24 additions & 8 deletions tests/_Test.php
Expand Up @@ -28,13 +28,11 @@ public function testReadmeExamples()
->value();
$this->assertSame(18, $avgMaleAge);

if (function_exists('array_column')) { // array_column() requires PHP 5.5+
$males = array_filter($people, function ($person) {
return $person['gender'] === 'male';
});
$avgMaleAge = array_sum(array_column($males, 'age')) / count($males);
$this->assertSame(18, $avgMaleAge);
}
$males = array_filter($people, function ($person) {
return $person['gender'] === 'male';
});
$avgMaleAge = array_sum(array_column($males, 'age')) / count($males);
$this->assertSame(18, $avgMaleAge);

/*
Ad-hoc operation
Expand All @@ -53,6 +51,7 @@ public function testReadmeExamples()
Data types
*/

// Array
$this->assertSame(
[4, 8],
Dash\chain([1, 2, 3, 4])
Expand All @@ -63,6 +62,7 @@ public function testReadmeExamples()
->value()
);

// Object
$this->assertSame(
'a, c',
Dash\chain((object) ['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4])
Expand All @@ -72,6 +72,7 @@ public function testReadmeExamples()
->value()
);

// Traversable
$this->assertSame(
5,
Dash\chain(new ArrayObject(['a' => 1, 'b' => 2, 'c' => 3, 'd' => 4]))
Expand All @@ -81,6 +82,22 @@ public function testReadmeExamples()
->value()
);

// Generator
$integers = function () {
for ($int = 0; true; $int++) {
yield $int;
}
};
$this->assertSame(
[5, 3, 1],
Dash\chain($integers())
->filter('Dash\isOdd')
->take(3)
->reverse()
->value()
);

// DirectoryIterator
$iterator = new \FilesystemIterator(__DIR__, \FilesystemIterator::SKIP_DOTS);
$filenames = Dash\chain($iterator)
->reject(function ($fileinfo) {
Expand All @@ -90,7 +107,6 @@ public function testReadmeExamples()
return pathinfo($fileinfo)['filename'];
})
->value();

$this->assertGreaterThan(10, count($filenames));

/*
Expand Down

0 comments on commit 400de52

Please sign in to comment.