stubbles/sequence

Stream your iterators.

v10.1.0 2023-12-25 19:24 UTC

README

Stream your iterators.

Build status

Tests Coverage Status

Latest Stable Version Latest Unstable Version

Installation

stubbles/sequence is distributed as Composer package. To install it as a dependency of your package use the following command:

composer require "stubbles/sequence": "^10.0"

Requirements

stubbles/sequence requires at least PHP 8.0.

Introduction

Sequence operations are divided into intermediate and terminal operations, and are combined to form pipelines. A pipeline consists of a source (such as a collection, an array, a generator function, or an I/O channel); followed by zero or more intermediate operations such as Sequence::filter() or Sequence::map(); and a terminal operation such as Sequence::each() or Sequence::reduce().

Intermediate operations return a new Sequence. They are always lazy; executing an intermediate operation such as Sequence::filter() does not actually perform any filtering, but instead creates a new Sequence that, when traversed, contains the elements of the initial stream that match the given predicate. Traversal of the pipeline source does not begin until the terminal operation of the pipeline is executed.

Terminal operations, such as Sequence::each() or Sequence::reduce(), may traverse the Sequence to produce a result or a side-effect. After the terminal operation is performed, the pipeline is considered consumed, and can no longer be used; if you need to traverse the same data source again, you must return to the data source to get a new Sequence. In almost all cases, terminal operations are eager, completing their traversal of the data source and processing of the pipeline before returning. Only the terminal operation Sequence::getIterator() is not; this is provided as an "escape hatch" to enable arbitrary client-controlled pipeline traversals in the event that the existing operations are not sufficient to the task.

Create a sequence

Sequence::of($elements)

Creates sequence of given $elements which can be either a \Traversable or an array.

Since release 8.1 it is possible to create a sequence in these ways:

  • no arguments: equivalent to Sequence::of([])
  • one argument which is an instance of Sequence: returns exactly this sequence
  • one argument which is an array or a \Traversable: sequence of this
  • one argument which is none of the above: equivalent to Sequence::of([$element])
  • two or more arguments: sequence of the list of arguments

Sequence::infinite($seed, callable $operation)

Creates an infinite sequence. With $seed the initial value can be specified, while $operation must be callable which takes the current value and generates the next value.

Warning: calling terminal operations on an infinite sequence result in endless loops trying to calculate the terminal value. Before calling a terminal operation the sequence should be limited via Sequence::limit(). Alternatively you can iterate over the sequence itself and stop the iteration when required.

Sequence::generate($seed, callable $operation, callable $validator)

Creates a sequence which generates values while being worked on.

The sequence ends when the provided validator returns false for the first time. The validator receives two values: the last generated value, and the amount of values already generated.

The following example generates an array which has $start as first value, where each following value is incremented by 2, and the amount of values in the array is either maximal 100 or PHP_INT_MAX has been reached:

Sequence::generate(
     $start,
     function($previous) { return $previous + 2; },
     function($value, $invocations) { return $value < (PHP_INT_MAX - 1) &&  100 >= $invocations; }
)->values();

Intermediate operations

limit($n)

Limits sequence to the first n elements, i.e. stops iteration when the nth element is reached.

Sequence::of([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])->limit(3)->data();

Result: [1, 2, 3]

skip($n)

Skips the first n elements of the sequence.

Sequence::of([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])->skip(10)->data();

Result: [11]

filter(callable $predicate)

Returns a new sequence with elements matching the given predicate. The given predicate reveives a value and must return true to accept the value or false to reject the value.

Sequence::of([1, 2, 3, 4, 5, 6, 7, 8, 9, 10])
        ->filter(function($value) { return $value % 2 === 0;})
        ->data();

Result: [2, 4, 6, 8, 10]

map(callable $valueMapper, callable $keyMapper = null)

Returns a new sequence which maps each element using the given mapper.

Sequence::of([1, 2, 3, 4])->map(function($e) { return $e * 2; })->data();

Result: [2, 4, 6, 8]

mapKeys(callable $keyMapper)

Returns a new sequence which maps each key using the given mapper.

Sequence::of([1, 2, 3, 4])->mapKeys(function($e) { return $e * 2; })->data();

Result: [0 => 1, 2 => 2, 4 => 3, 6 => 4]

append($other)

Appends any value, creating a new combined sequence.

In case given $other is not something iterable it is simply appended as last element to a new sequence.

Sequence::of([1, 2])->append([3, 4]); // results in new sequence with [1, 2, 3, 4]

peek(callable $valueConsumer, callable $keyConsumer = null)

Allows consumer to receive the value before any further operations are applied.

You can use it to inspect the values and keys before any further operations are applied. This is especially useful when you need to debug the contents of a sequence.

Sequence::of([1, 2, 3, 4])->peek('var_dump');

Terminal operations

each(callable $consumer)

Invokes consumer for each element and returns the amount of invocations.

The consumer receives the element as first value, and the key as second:

Sequence::of(['foo' => 'bar'])->each(
        function($element, $key)
        {
            // do something with $element and $key
        }
);

The key is optional and can be left away:

Sequence::of(['foo' => 'bar'])->each(
        function($element)
        {
            // do something with $element and $key
        }
);

Iteration can be stopped by returning false from the consumer. The following example stops when it reaches element 2:

Sequence::of([1, 2, 3, 4])->each(
        function($element)
        {
            echo $element . "\n";
            return (2 <= $element);
        }
);

first()

Returns first element of sequence.

echo Sequence::of(['foo', 'bar', 'baz'])->first(); // displays 'foo'

reduce(callable $accumulate = null, $identity = null)

Reduces all elements of the sequence to a single value. The given callable will receive two values: the current reduced value which on first invocation is the value of $identity, and the current element as second. It needs to calculate and return a new value from both which becomes the new value of identity, and will be returned from reduce() after the last element has been processed.

Sequence::of([1, 2, 3, 4])->reduce(function($identity, $b) { return $identity + $b; });

In case no callable is provided an instance of stubbles\sequence\Reducer will be returned which provides convenience methods for some common reduction operations.

reduce()->toSum(callable $summer = null)

Reduces sequence to the sum of all elements. By default assumes the sequence consists of numbers and simply adds them one after another.

Sequence::of([1, 2, 3, 4])->reduce()->toSum();

In case the sequence consists of other types a callable can be passed that can calculate the sum instead. The callable must expect two values: the sum calculated until now and a single element. The return value must be the new sum with the given element.

Sequence::of(['a', 'b', 'c', 'd'])->reduce()->toSum(
        function($sum, $element)
        {
            return $sum + ord($element);
        }
);

reduce()->toMin(callable $min = null)

Reduces sequence to the smallest element. By default assumes the sequence consists of numbers.

Sequence::of([1, 2, 3, 4])->reduce()->toMin();

In case the sequence consists of other types a callable can be passed that can calculate the smallest value instead. The callable must expect two values: the smalles value found until until now (which null on the first invocation) and a single element. The return value must be the smaller of both arguments.

Sequence::of(['a', 'b', 'c', 'd'])->reduce()->toSum(
        function($smallest, $element)
        {
            return (null === $smallest || ord($element) < ord($smallest)) ? $element : $smallest;
        }
);

reduce()->toMax(callable $max = null)

Reduces sequence to the greatest element. By default assumes the sequence consists of numbers.

Sequence::of([1, 2, 3, 4])->reduce()->toMax();

In case the sequence consists of other types a callable can be passed that can calculate the greatest value instead. The callable must expect two values: the greatest value found until until now (which null on the first invocation) and a single element. The return value must be the greater of both arguments.

Sequence::of(['a', 'b', 'c', 'd'])->reduce()->toMax(
        function($greatest, $element)
        {
            return (null === $greatest || ord($element) > ord($greatest)) ? $element : $greatest;
        }
);

collect(Collector $collector = null)

Collects all elements into a structure defined by given collector.

A collector accumulates elements into a structure, optionally transforming the result into a final representation.

In case no collector is provided an instance of stubbles\sequence\Collectors will be returned which provides convenience methods for some common collector operations.

collect()->inList()

Returns the values of the sequence as array.

Sequence::of(['foo' => 'bar', 'dummy' => 'baz'])->collect()->inList(); // returns ['bar', 'baz']

collect()->inMap(callable $selectKey = null, callable $selectValue = null)

Returns the sequence data with keys and values as associative array. The $selectKey callable will be used to determine the key for a value in the new map, and $selectValue will be used to determine the value. If they are omitted the key and value from the source elements will be used as they are.

$people= [
        1549 => new Employee(1549, 'Timm', 'B', 15),
        1552 => new Employee(1552, 'Alex', 'I', 14),
        6100 => new Employee(6100, 'Dude', 'I', 4)
];
$employees = Sequence::of($people)->collect()->inMap(
        function(Employee $e) { return $e->id(); },
        function(Employee $e) { return $e->name(); }
); // results in [1549 => 'Timm', 1552 => 'Alex', 6100 => 'Dude']

collect()->inPartitions(callable $predicate, Collector $base = null)

Groups the elements in two partitions according to given predicate.

$timm = new Employee(1549, 'Timm', 'B', 15);
$alex = new Employee(1552, 'Alex', 'I', 14);
$dude = new Employee(6100, 'Dude', 'I', 4);
$employees = Sequence::of([$timm, $alex, $dude])->collect()->inPartitions(
        function(Employee $e) { return $e->years() > 10; }
);  // results in [true  => [$timm, $alex], false => [$dude]]

The second argument can be used to influence the actual partition value.

$timm = new Employee(1549, 'Timm', 'B', 15);
$alex = new Employee(1552, 'Alex', 'I', 14);
$dude = new Employee(6100, 'Dude', 'I', 4);
$employees = Sequence::of([$timm, $alex, $dude])->collect()->inPartitions(
        function(Employee $e) { return $e->years() > 10; },
        Collector::forAverage(function(Employee $e) { return $e->years(); })
);  // results in [true  => 14.5, false => 4]

collect()->inGroups(callable $classifier, Collector $base = null)

Groups the elements in two partitions according to given predicate.

$timm = new Employee(1549, 'Timm', 'B', 15);
$alex = new Employee(1552, 'Alex', 'I', 14);
$dude = new Employee(6100, 'Dude', 'I', 4);
$employees = Sequence::of([$timm, $alex, $dude])->collect()->inGroups(
        function(Employee $e) { return $e->department(); }
); // results in ['B' => [$timm], 'I' => [$alex, $dude]]

The second argument can be used to influence the actual group value:

$timm = new Employee(1549, 'Timm', 'B', 15);
$alex = new Employee(1552, 'Alex', 'I', 14);
$dude = new Employee(6100, 'Dude', 'I', 4);
$employees = Sequence::of([$timm, $alex, $dude])->collect()->inGroups(
        function(Employee $e) { return $e->department(); },
        Collector::forSum(function(Employee $e) { return $e->years(); })
); // results in ['B' => 15, 'I' => 18]

collect()->byJoining($delimiter = ', ', $prefix = '', $suffix = '', $keySeparator = null)

Concatenates all elements into a single string.

$timm = new Employee(1549, 'Timm', 'B', 15);
$alex = new Employee(1552, 'Alex', 'I', 14);
$dude = new Employee(6100, 'Dude', 'I', 4);
$employees = Sequence::of([$timm, $alex, $dude])
        ->map(function(Employee $e) { return $e->name(); })
        ->collect()
        ->byJoining();
// results in 'Timm, Alex, Dude'

When $keySeparator is supplied the key will also be included:

$timm = new Employee(1549, 'Timm', 'B', 15);
$alex = new Employee(1552, 'Alex', 'I', 14);
$dude = new Employee(6100, 'Dude', 'I', 4);
$employees = Sequence::of([1549 => $timm, 1552 => $alex, 6100 => $dude])
        ->map(function(Employee $e) { return $e->name(); })
        ->collect()
        ->byJoining(', ', '(', ')', ':');
// results in '(1549:Timm, 1552:Alex, 6100:Dude)'

count()

Returns number of elements in sequence.

echo Sequence::of(['foo', 'bar', 'baz'])->count(); // displays 3

As Sequence is also an instance of \Countable it can also be used with PHP's native count() function:

echo count(Sequence::of(['foo', 'bar', 'baz'])); // displays 3

values()

Returns the values of the sequence as array, shortcut for collect()->inList().

Sequence::of(['foo' => 'bar', 'dummy' => 'baz'])->values(); // returns ['bar', 'baz']

data()

Returns the sequence data with keys and values as associative array. Shortcut for collect()->inMap().

Sequence::of(['foo' => 'bar', 'dummy' => 'baz'])->data(); // returns ['foo' => 'bar', 'dummy' => 'baz']

Sequence validation with bovigo/assert

Available since release 8.0.0

In case you use bovigo/assert for assertions in your unit tests stubbles/sequence provides two predicates which can be used to ensure a sequence contains the expected data:

assertThat($yourSequence, Provides::values([1, 2, 3]));
assertThat($yourSequence, Provides::data(['foo' => 1, 'bar' => 2, 'baz' => 3]));

Both are available with the class stubbles\sequence\assert\Provides. While the first one checks values only and does not consider the keys, the second also checks the keys. Please note that both check exactly those elements - if the sequence contains more values the predicated will fail.