Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce append(), prepend(), push(), unshift() #108

Merged
merged 11 commits into from
Nov 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ jobs:
runs-on: ubuntu-latest

env:
PHP_VERSION: 8.0
PHP_VERSION: '8.1'
COMPOSER_ROOT_VERSION: v5.99

steps:
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ COMPOSER=$(PHP) $(shell which composer)

# Infection
INFECTION=vendor/bin/infection
MIN_MSI=100
MIN_COVERED_MSI=100
MIN_MSI=90
MIN_COVERED_MSI=90
INFECTION_ARGS=--min-msi=$(MIN_MSI) --min-covered-msi=$(MIN_COVERED_MSI) --threads=$(JOBS) --coverage=build/logs --log-verbosity=default --show-mutations --no-interaction

all: test
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ All entry points always return an instance of the pipeline.
| Method | Details | Use with |
| ----------- | ----------------------------- | ----------- |
| `map()` | Takes an optional initial callback, where it must not require any arguments. Other than that, works just like an instance method below. | `use function Pipeline\map;` |
| `take()` | Takes any iterable, including arrays, initializes a pipeline with it. | `use function Pipeline\take;` |
| `take()` | Takes any iterables, including arrays, joining them together in succession. | `use function Pipeline\take;` |
| `fromArray()` | Takes an array, initializes a pipeline with it. | `use function Pipeline\fromArray;` |
| `zip()` | Takes an iterable, and several more, merging them together. | `use function Pipeline\zip;` |

Expand All @@ -108,6 +108,10 @@ All entry points always return an instance of the pipeline.
| ----------- | ----------------------------- | ----------------- |
| `map()` | Takes an optional callback that for each input value may return one or yield many. Also takes an initial generator, where it must not require any arguments. Provided no callback does nothing. Also available as a plain function. | `SelectMany` |
| `cast()` | Takes a callback that for each input value expected to return another single value. Unlike `map()`, it assumes no special treatment for generators. Provided no callback does nothing. | `array_map`, `Select` |
| `append()` | Appends the contents of an interable to the end of the pipeline. | `array_merge` |
| `push()` | Appends the arguments to the end of the pipeline. | `array_push` |
| `prepend()` | Appends the contents of an interable to the end of the pipeline. | `array_merge` |
| `unshift()` | Prepends the pipeline with a list of values. | `array_unshift` |
| `zip()` | Takes a number of iterables, merging them together with the current sequence, if any. | `array_map(null, ...$array)`, Python's `zip()`, transposition |
| `unpack()` | Unpacks arrays into arguments for a callback. Flattens inputs if no callback provided. | `flat_map`, `flatten` |
| `filter()` | Removes elements unless a callback returns true. Removes falsey values if no callback provided. | `array_filter`, `Where` |
Expand Down
141 changes: 130 additions & 11 deletions src/Standard.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,13 @@
use function array_filter;
use function array_flip;
use function array_map;
use function array_merge;
use function array_reduce;
use function array_shift;
use function array_slice;
use function array_values;
use ArrayIterator;
use function assert;
use CallbackFilterIterator;
use function count;
use Countable;
Expand Down Expand Up @@ -72,13 +74,129 @@ public function __construct(iterable $input = null)
$this->pipeline = $input;
}

/**
* Appends the contents of an interable to the end of the pipeline.
*
* @param ?iterable $values
*/
public function append(iterable $values = null): self
{
// Do we need to do anything here?
if ($this->willReplace($values)) {
return $this;
}

// Static analyzer hints
assert(null !== $this->pipeline);
assert(null !== $values);

return $this->join($this->pipeline, $values);
}

/**
* Appends a list of values to the end of the pipeline.
*
* @param mixed ...$vector
*/
public function push(...$vector): self
{
return $this->append($vector);
}

/**
* Prepends the pipeline with the contents of an iterable.
*
* @param ?iterable $values
*/
public function prepend(iterable $values = null): self
{
// Do we need to do anything here?
if ($this->willReplace($values)) {
return $this;
}

// Static analyzer hints
assert(null !== $this->pipeline);
assert(null !== $values);

return $this->join($values, $this->pipeline);
}

/**
* Prepends the pipeline with a list of values.
*
* @param mixed ...$vector
*/
public function unshift(...$vector): self
{
return $this->prepend($vector);
}

/**
* Determine if the internal pipeline will be replaced when appending/prepending.
*
* Utility method for appending/prepending methods.
*/
private function willReplace(iterable $values = null): bool
{
// Nothing needs to be done here.
/** @phan-suppress-next-line PhanTypeComparisonFromArray */
if (null === $values || [] === $values) {
return true;
}

// No shortcuts are applicable if the pipeline was initialized.
if ([] !== $this->pipeline && null !== $this->pipeline) {
return false;
}

// Install an array as it is.
if (is_array($values)) {
$this->pipeline = $values;

return true;
}

// Else we use ownself to handle edge cases.
$this->pipeline = new self($values);

return true;
}

/**
* Replace the internal pipeline with a combination of two non-empty iterables, array-optimized.
*
* Utility method for appending/prepending methods.
*/
private function join(iterable $left, iterable $right): self
{
// We got two arrays, that's what we will use.
if (is_array($left) && is_array($right)) {
$this->pipeline = array_merge($left, $right);

return $this;
}

// Last, join the hard way.
$this->pipeline = self::joinYield($left, $right);

return $this;
}

/**
* Replace the internal pipeline with a combination of two non-empty iterables, generator-way.
*/
private static function joinYield(iterable $left, iterable $right): iterable
{
yield from $left;
yield from $right;
}

/**
* An extra variant of `map` which unpacks arrays into arguments. Flattens inputs if no callback provided.
*
* @param ?callable $func
*
* @psalm-suppress InvalidArgument
*
* @return $this
*/
public function unpack(?callable $func = null): self
Expand All @@ -88,6 +206,7 @@ public function unpack(?callable $func = null): self
};

return $this->map(static function (iterable $args = []) use ($func) {
/** @psalm-suppress InvalidArgument */
return $func(...$args);
});
}
Expand All @@ -109,7 +228,6 @@ public function map(?callable $func = null): self

// That's the standard case for any next stages.
if (is_iterable($this->pipeline)) {
/** @phan-suppress-next-line PhanTypeMismatchArgument */
$this->pipeline = self::apply($this->pipeline, $func);

return $this;
Expand Down Expand Up @@ -178,7 +296,6 @@ public function cast(callable $func = null): self
}

if (is_iterable($this->pipeline)) {
/** @phan-suppress-next-line PhanTypeMismatchArgument */
$this->pipeline = self::applyOnce($this->pipeline, $func);

return $this;
Expand Down Expand Up @@ -231,11 +348,10 @@ public function filter(?callable $func = null): self
return $this;
}

/** @var Iterator $iterator */
$iterator = $this->pipeline;
assert($this->pipeline instanceof Iterator);

/** @phan-suppress-next-line PhanTypeMismatchArgumentInternal */
$this->pipeline = new CallbackFilterIterator($iterator, $func);
/** @psalm-suppress ArgumentTypeCoercion */
$this->pipeline = new CallbackFilterIterator($this->pipeline, $func);

return $this;
}
Expand Down Expand Up @@ -390,9 +506,12 @@ private static function makeNonRewindable(iterable $input): Generator
return $input;
}

return (static function (iterable $input) {
yield from $input;
})($input);
return self::generatorFromIterable($input);
}

private static function generatorFromIterable(iterable $input): Generator
{
yield from $input;
}

/**
Expand Down
18 changes: 16 additions & 2 deletions src/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,30 @@ function map(callable $func = null): Standard
return $pipeline->map($func);
}

function take(iterable $input = null): Standard
function take(iterable $input = null, iterable ...$inputs): Standard
{
return new Standard($input);
$pipeline = new Standard($input);

foreach ($inputs as $input) {
$pipeline->append($input);
}

return $pipeline;
}

function fromArray(array $input): Standard
{
return new Standard($input);
}

/**
* @param mixed ...$values
*/
function fromValues(...$values): Standard
{
return new Standard($values);
}

function zip(iterable $base, iterable ...$inputs): Standard
{
$result = take($base);
Expand Down
Loading