Usage

Find here some working examples.

Tip

Read the section on Working with keys and values to better understand the differences between working with Collection compared to normal PHP arrays. The same principles apply to all API methods, giving you great power to manipulate various types of data if used correctly.

Simple

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$collection = Collection::fromIterable(['A', 'B', 'C', 'D', 'E']);

// Get the result as an array.
$collection
    ->all(); // ['A', 'B', 'C', 'D', 'E']

// Get the first item.
$collection
    ->first()
    ->current(); // 'a'

// Append items.
$collection
    ->append('F', 'G', 'H')
    ->all(); // ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']

// Prepend items.
$collection
    ->prepend('1', '2', '3')
    ->all(); // ['1', '2', '3', 'A', 'B', 'C', 'D', 'E']

// Split a collection into chunks of a given size.
$collection
    ->chunk(2)
    ->all(); // [['A', 'B'], ['C', 'D'], ['E']]

// Merge items.
$collection
    ->merge([1, 2], [3, 4], [5, 6])
    ->all(); // ['A', 'B', 'C', 'D', 'E', 1, 2, 3, 4, 5, 6]

// Map data
$collection
    ->map(
        static fn ($value, $key): string => sprintf('%s.%s', $value, $value)
    )
    ->all(); // ['A.A', 'B.B', 'C.C', 'D.D', 'E.E']

// Use \StdClass as input
$data = (object) array_combine(range('A', 'E'), range('A', 'E'));

// Keys are preserved during the map() operation.
Collection::fromIterable(['A' => 'A', 'B' => 'B', 'C' => 'C', 'D' => 'D', 'E' => 'E'])
    ->map(
        static fn (string $value): string => strtolower($value)
    )
    ->all(); // ['A' => 'a', B => 'b', 'C' => 'c', 'D' = >'d', 'E' => 'e']

// Tail
Collection::fromIterable(range('a', 'e'))
    ->tail()
    ->all(); // [1 => 'b', 2 => 'c', 3 => 'd', 4 => 'e']

// Reverse
Collection::fromIterable(range('a', 'e'))
    ->tail()
    ->reverse()
    ->all(); // [4 => 'e', 3 => 'd', 2 => 'c', 1 => 'b']

// Flip operation.
// array_flip() can be used in PHP to remove duplicates from an array.(dedup-licate an array)
// See: https://www.php.net/manual/en/function.array-flip.php
// Example:
// $dedupArray = array_flip(array_flip(['a', 'b', 'c', 'd', 'a'])); // ['a', 'b', 'c', 'd']
// However, in loophp/collection it doesn't behave as such.
// As this library is based on PHP Generators, it's able to return multiple times the same key when iterating.
// You end up with the following result when issuing twice the ::flip() operation.
Collection::fromIterable(['a', 'b', 'c', 'd', 'a'])
    ->flip()
    ->flip()
    ->all(); // ['a', 'b', 'c', 'd', 'a']

// Get the Cartesian product.
Collection::fromIterable(['a', 'b'])
    ->product([1, 2])
    ->all(); // [['a', 1], ['a', 2], ['b', 1], ['b', 2]]

// Infinitely loop over numbers, cube them, filter those that are not divisible by 5, take the first 100 of them.
Collection::range(0, \INF)
    ->map(static fn ($value): float => $value ** 3)
    ->filter(static fn ($value): int => $value % 5)
    ->limit(100)
    ->all(); // [1, 8, 27, ..., 1815848, 1860867, 1906624]

// Apply a callback to the values without altering the original object.
// If the callback returns false, then it will stop.
Collection::fromIterable(range('A', 'Z'))
    ->apply(
        static function ($value, $key): bool {
            echo mb_strtolower($value);

            return true;
        }
    );

// Generate 300 distinct random numbers between 0 and 1000
$random = static fn (): array => [random_int(0, mt_getrandmax()) / mt_getrandmax()];

Collection::unfold($random)
    ->unwrap()
    ->map(static fn ($value): float => floor($value * 1000) + 1)
    ->distinct()
    ->limit(300)
    ->all();

// Fibonacci using the static method ::unfold()
$fibonacci = static fn ($a = 0, $b = 1): array => [$b, $b + $a];

Collection::unfold($fibonacci)
    // Get the first item of each result.
    ->pluck(0)
    // Limit the amount of results to 10.
    ->limit(10)
    // Convert to regular array.
    ->all(); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Collection::unfold($fibonacci)
    ->map(static fn (array $value) => $value[1] / $value[0])
    ->limit(100)
    ->last(); // 1.6180339887499

// Use an existing Generator as input data.
$readFileLineByLine = static function (string $filepath): Generator {
    /** @var resource $fh */
    $fh = fopen($filepath, 'rb');

    while (false !== $line = fgets($fh)) {
        yield $line;
    }

    fclose($fh);
};

$hugeFile = __DIR__ . '/vendor/composer/autoload_static.php';

Collection::fromIterable($readFileLineByLine($hugeFile))
    // Add the line number at the end of the line, as comment.
    ->map(
        static fn ($value, $key): string => str_replace(\PHP_EOL, ' // line ' . $key . \PHP_EOL, $value)
    )
    // Find public static fields or methods among the results.
    ->filter(
        static fn ($value, $key): bool => false !== mb_strpos(trim($value), 'public static')
    )
    // Drop the first result.
    ->drop(1)
    // Limit to 3 results only.
    ->limit(3)
    // Implode into a string.
    ->implode();

// Load a string
$string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
  Quisque feugiat tincidunt sodales.
  Donec ut laoreet lectus, quis mollis nisl.
  Aliquam maximus, orci vel placerat dapibus, libero erat aliquet nibh, nec imperdiet felis dui quis est.
  Vestibulum non ante sit amet neque tincidunt porta et sit amet neque.
  In a tempor ipsum. Duis scelerisque libero sit amet enim pretium pulvinar.
  Duis vitae lorem convallis, egestas mauris at, sollicitudin sem.
  Fusce molestie rutrum faucibus.';

// By default will have the same behaviour as str_split().
Collection::fromString($string)
    ->explode(' ')
    ->count(); // 71

// Or add a separator if needed, same behaviour as explode().
Collection::fromString($string, ',')
    ->count(); // 9

// Regular values normalization.
Collection::fromIterable([0, 2, 4, 6, 8, 10])
    ->scale(0, 10)
    ->all(); // [0, 0.2, 0.4, 0.6, 0.8, 1]

// Logarithmic values normalization.
Collection::fromIterable([0, 2, 4, 6, 8, 10])
    ->scale(0, 10, 5, 15, 3)
    ->all(); // [5, 8.01, 11.02, 12.78, 14.03, 15]

// Fun with function convergence.
// Iterator over the function: f(x) = r * x * (1-x)
// Change that parameter $r to see different behaviour.
// More on this: https://en.wikipedia.org/wiki/Logistic_map
$function = static fn ($x = .3, $r = 2): array => [$r * $x * (1 - $x)];

Collection::unfold($function)
    ->unwrap()
    ->map(static fn ($value): float => round($value, 2))
    ->limit(10)
    ->all(); // [0.42, 0.48, 0.49, 0.49, 0.5, 0.5, 0.5, 0.5, 0.5, 0.5]

// Infinitely loop over a collection
Collection::fromIterable(['A', 'B', 'C'])
    ->cycle();

// Traverse the collection using windows of a given size.
Collection::fromIterable(range('a', 'z'))
    ->window(3)
    ->all(); // [['a'], ['a', 'b'], ['a', 'b', 'c'], ['b', 'c', 'd'], ['c', 'd', 'e'], ...]

Collection::fromIterable(range('a', 'd'))
    ->wrap()
    ->all(); // [['a'], ['b'], ['c'], ['d']]

Collection::fromIterable([['a'], ['b'], ['c'], ['d']])
    ->unwrap()
    ->all(); // ['a', 'b', 'c', 'd']

Advanced

Working with keys and values

This example shows the power of a lazy library and highlight also how to use it in the wrong way.

Unlike regular PHP arrays where there can only be one key of type int or string, a lazy library can have multiple times the same keys and they can be of any type!

// This following example is perfectly valid, despite that having array for
// keys in a regular PHP arrays is impossible.
$input = static function () {
    yield ['a'] => 'a';
    yield ['b'] => 'b';
    yield ['c'] => 'c';
};
Collection::fromIterable($input());

A lazy collection library can also have multiple times the same key.

Here we are going to make a frequency analysis on the text and see the result. We can see that some data is missing, why?

$string = 'aaaaabbbbcccddddeeeee';

$collection = Collection::fromString($string)
    // Run the frequency analysis tool.
    ->frequency()
    // Convert to regular array.
    ->all(false); // [5 => 'e', 4 => 'd', 3 => 'c']

The reason that the frequency analysis for letters ‘a’ and ‘b’ is missing is because when you call the method Collection::all() with a false parameter, the collection converts the lazy collection into a regular PHP array, and PHP doesn’t allow having multiple time the same key; thus, it overrides the previous data and there will be missing information in the resulting array.

In order to prevent this, by default the all operation will also apply normalize, re-indexing and re-ordering the keys. However, this might not always be the desired outcome, like in this instance (see examples below).

Other ways to circumvent this PHP array limitation:

  • use the wrap operation on the final result, wrapping each key-value pair in a PHP array

  • do not convert the collection to an array and use it as an iterator instead

It’s up to you to decide which approach to take based on your use case.

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$frequency = Collection::fromString('aaaaabbbbcccddddeeeee')
    ->frequency();

// Method 1 -> wrapped collection
$wrapped = $frequency
    ->wrap() // Wrap each result into an array
    ->all(); // Convert to regular array

/**
 * [
 *   [5 => 'a'],
 *   [4 => 'b'],
 *   [3 => 'c'],
 *   [4 => 'd'],
 *   [5 => 'e'],
 * ].
 */

// Method 2 -> normalized collection
$normalized = $frequency
    ->all(); // Convert to regular array and replace keys with numerical indexes

/**
 * [0 => 'a', 1 => 'b', 2 => 'c', 3 => 'd', 4 => 'e'].
 *
 * Note this is not useful for our frequency analysis scenario,
 * but it can be useful in cases where we don't care about the
 * key values, like in the example below.
 */
$collection = Collection::fromIterable(range(1, 10))
    ->filter(static fn ($value): bool => $value % 3 === 0);

$filtered = $collection->all(false); // [2 => 3, 5 => 6, 8 => 9]
$filteredNormalized = $collection->all(); // [0 => 3, 1 => 6, 2 => 9]

// Method 3 -> consuming the collection as an iterator
foreach ($frequency as $k => $v) {
    var_dump("({$k}, {$v})");
}

/**
 * "(5, a)"
 * "(4, b)"
 * "(3, c)"
 * "(4, d)"
 * "(5, e)".
 */

Serialization

The collection object implements the JsonSerializable interface, thus allowing for JSON serialization using the built-in PHP function json_encode or a custom serializer like the Symfony Serializer.

Tip

By default the collection is not normalized when serializing, which allows its usage as an associative array. However, when it is used as a list and there are missing keys, the normalize operation should be applied before serialization; not doing so will likely not result in the desired outcome, as shown in the example below.

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

// Example 1 -> using `json_encode`

// a) with list, ordered keys
json_encode(Collection::fromIterable([1, 2, 3]), \JSON_THROW_ON_ERROR); // JSON: '[1, 2, 3]'

// b) with list, missing keys
$col = Collection::fromIterable([1, 2, 3])
    ->filter(static fn (int $val): bool => $val % 2 !== 0); // [0 => 1, 2 => 3]

json_encode($col, \JSON_THROW_ON_ERROR); // JSON: '{"0": 1, "2": 3}'

// c) with list, missing keys, with `normalize`
$col = Collection::fromIterable([1, 2, 3])
    ->filter(static fn (int $val): bool => $val % 2 !== 0)
    ->normalize(); // [0 => 1, 1 => 3]

json_encode($col, \JSON_THROW_ON_ERROR); // JSON: '[1, 3]'

// d) with associative array
json_encode(Collection::fromIterable(['foo' => 1, 'bar' => 2]), \JSON_THROW_ON_ERROR); // JSON: '{"foo": 1, "bar": 2}'

// e) with associative array, with `normalize`

$col = Collection::fromIterable(['foo' => 1, 'bar' => 2])
    ->normalize(); // [0 => 1, 1 => 2]

json_encode($col, \JSON_THROW_ON_ERROR); // JSON: '[1, 2]'

// Example 2 -> using custom serializer (all previous behaviours apply)

/** @var Symfony\Component\Serializer\Serializer $serializer */
$serializer = new Serializer(); // parameters omitted for brevity

$serializer->serialize(Collection::fromIterable([1, 2, 3]), 'json'); // JSON: '[1, 2, 3]'

Extending collection

Sometimes, it is possible that the feature set provided by this library is not enough. Then, you would like to create your own collection class with some specific feature added on top of it.

If you want to extend the Collection, you have multiple options.

  1. You just create a callable and use pipe method.

  2. You use the Composition design pattern and create your own library class.

  3. Use the CollectionDecorator abstract class available since version 7.0.0

Every classes of this library are final and then it is impossible to use inheritance and use the original Collection class as parent of another one.

You can read more about Inheritance and Composition by doing a query on your favorite search engine. I also wrote an article about this.

Find here an example on how you could extend the collection class and add a new operation foobar.

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;
use loophp\collection\Contract\Collection as CollectionInterface;
use loophp\collection\Operation\AbstractOperation;
use loophp\collection\Operation\Append;

interface FoobarCollectionInterface extends CollectionInterface
{
    public function foobar(): FoobarCollectionInterface;
}

final class Foobar extends AbstractOperation
{
    public function __invoke(): Closure
    {
        return static function (Iterator $iterator): Generator {
            foreach ($iterator as $key => $value) {
                yield 'foo' => 'bar';
            }
        };
    }
}

final class FoobarCollection implements FoobarCollectionInterface
{
    private CollectionInterface $collection;

    public function __construct(callable $callable, ...$parameters)
    {
        $this->collection = new Collection($callable, ...$parameters);
    }

    public function append(...$items): FoobarCollectionInterface
    {
        return new self(Append::of()(...$items), $this->collection);
    }

    public function foobar(): FoobarCollectionInterface
    {
        return new self(Foobar::of(), $this->collection);
    }

    // This example is intentionally incomplete.
    // For the sake of brevity, I did not add all the remaining
    // methods to satisfy the FoobarCollectionInterface.
}

Since version 7.0.0, we ship a CollectionDecorator abstract class which let you create your own Collection class with all your customizations.

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\CollectionDecorator;

final class FoobarCollection extends CollectionDecorator
{
    public function toUpperCase(): FoobarCollection
    {
        return $this
            ->map(
                static fn (string $letter): string => strtoupper($letter)
            );
    }
}

$collection = FoobarCollection::fromIterable(range('a', 'e'))
    ->toUpperCase();

Manipulate strings

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.
      Quisque feugiat tincidunt sodales.
      Donec ut laoreet lectus, quis mollis nisl.
      Aliquam maximus, orci vel placerat dapibus, libero erat aliquet nibh, nec imperdiet felis dui quis est.
      Vestibulum non ante sit amet neque tincidunt porta et sit amet neque.
      In a tempor ipsum. Duis scelerisque libero sit amet enim pretium pulvinar.
      Duis vitae lorem convallis, egestas mauris at, sollicitudin sem.
      Fusce molestie rutrum faucibus.';

// By default will have the same behaviour as str_split().
$count = Collection::fromString($string)
    ->explode(' ')
    ->count(); // 107

var_dump($count);

// Or add a separator if needed, same behaviour as explode().
$count = Collection::fromString($string, ',')
    ->count(); // 8

var_dump($count);

Random number generation

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

// Generate 300 distinct random numbers between 0 and 1000
$random = static fn (): array => [random_int(0, mt_getrandmax()) / mt_getrandmax()];

$random_numbers = Collection::unfold($random)
    ->unwrap()
    ->map(static fn ($value): float => floor($value * 1000) + 1)
    ->distinct()
    ->limit(300)
    ->all();

print_r($random_numbers);

Approximate the number e

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$multiplication = static fn (float $value1, float $value2): float => $value1 * $value2;

$addition = static fn (float $value1, float $value2): float => $value1 + $value2;

$fact = static fn (float $number): float => (float) Collection::range(1, $number + 1)
    ->foldLeft($multiplication, 1);

$number_e_approximation = Collection::unfold(static fn (int $i = 0): array => [$i + 1])
    ->unwrap()
    ->map(static fn (float $value): float => $value / $fact($value))
    ->until(static fn (float $value): bool => 10 ** -12 > $value)
    ->foldLeft($addition, 0);

var_dump($number_e_approximation); // 2.718281828459

Approximate the number Pi

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$monteCarloMethod = static function ($in = 0, $total = 1): array {
    $randomNumber1 = random_int(0, mt_getrandmax() - 1) / mt_getrandmax();
    $randomNumber2 = random_int(0, mt_getrandmax() - 1) / mt_getrandmax();

    if (1 >= (($randomNumber1 ** 2) + ($randomNumber2 ** 2))) {
        ++$in;
    }

    return ['in' => $in, 'total' => ++$total];
};

$pi_approximation = Collection::unfold($monteCarloMethod)
    ->map(static fn ($value) => 4 * $value['in'] / $value['total'])
    ->window(1)
    ->drop(1)
    ->until(static fn (array $value): bool => 0.00001 > abs($value[0] - $value[1]))
    ->unwrap()
    ->last();

print_r($pi_approximation->all()); // [3.14...]

Approximate the golden ratio

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

// Golden ratio: https://en.wikipedia.org/wiki/Golden_ratio
$goldenNumberGenerator = static fn ($a = 0): array => [($a + 1) ** .5];

$goldenNumber = Collection::unfold($goldenNumberGenerator)
    ->limit(10)
    ->unwrap()
    ->last();

var_dump($goldenNumber->current()); // 1.6180165422314876

Fibonacci sequence

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$fibonacci = static fn (int $a = 0, int $b = 1): array => [$b, $b + $a];

$c = Collection::unfold($fibonacci)
    ->pluck(0)    // Get the first item of each result.
    ->limit(10);  // Limit the amount of results to 10.

print_r($c->all()); // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]

Gamma function

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$addition = static fn (float $value1, float $value2): float => $value1 + $value2;

$listInt = static function (int $init, callable $succ): Generator {
    yield $init;

    while (true) {
        yield $init = $succ($init);
    }
};

$N = $listInt(1, static fn (int $n): int => $n + 1);

$Y = static fn (float $n): Closure => static fn (int $x): float => ($x ** ($n - 1)) * (\M_E ** (-$x));

$e = static fn (float $value): bool => 10 ** -12 > $value;

// Find the factorial of this number. This is not bounded to integers!
// $number = 3; // 2 * 2 => 4
// $number = 6; // 5 * 4 * 3 * 2 => 120
$number = 5.75; // 78.78

$gamma_factorial_approximation = Collection::fromIterable($N)
    ->map($Y($number))
    ->until($e)
    ->foldLeft($addition, 0);

print_r($gamma_factorial_approximation);

Prime numbers

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$primesGenerator = static function (Iterator $iterator) use (&$primesGenerator): Generator {
    yield $primeNumber = $iterator->current();

    $iterator = new CallbackFilterIterator(
        $iterator,
        static fn (int $a): bool => 0 !== $a % $primeNumber
    );

    $iterator->next();

    return $iterator->valid() ?
        yield from $primesGenerator($iterator) :
        null;
};

$integerGenerator = static function (int $init, callable $succ) use (&$integerGenerator): Generator {
    yield $init;

    return yield from $integerGenerator($succ($init), $succ);
};

$limit = 1_000_000;

$primes = $primesGenerator(
    $integerGenerator(
        2,
        static fn (int $n): int => $n + 1
    )
);

// Create a lazy collection of Prime numbers from 2 to infinity.
$lazyPrimeNumbersCollection = Collection::fromIterable(
    $primesGenerator(
        $integerGenerator(
            2,
            static fn (int $n): int => $n + 1
        )
    )
);

// Print out the first 1 million of prime numbers.
foreach ($lazyPrimeNumbersCollection->limit($limit) as $prime) {
    var_dump($prime);
}

// Create a lazy collection of Prime numbers from 2 to infinity.
$lazyPrimeNumbersCollection = Collection::fromIterable(
    $primesGenerator(
        $integerGenerator(
            2,
            static fn (int $n): int => $n + 1
        )
    )
);

// Find out the Twin Prime numbers by filtering out unwanted values.
$lazyTwinPrimeNumbersCollection = Collection::fromIterable($lazyPrimeNumbersCollection)
    ->zip($lazyPrimeNumbersCollection->tail())
    ->filter(
        static fn (array $chunk): bool => 2 === $chunk[1] - $chunk[0]
    );

foreach ($lazyTwinPrimeNumbersCollection->limit($limit) as $prime) {
    var_dump($prime);
}

Text analysis

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

/** @var string $contents */
$contents = file_get_contents('https://loripsum.net/api');

$collection = Collection::fromString($contents)
    // Filter out some characters.
    ->filter(static fn ($item): bool => (bool) preg_match('/^[a-zA-Z]+$/', $item))
    // Lowercase each character.
    ->map(static fn (string $letter): string => mb_strtolower($letter))
    // Run the frequency tool.
    ->frequency()
    // Flip keys and values.
    ->flip()
    // Sort values.
    ->sort()
    // Convert to array.
    ->all(false);

print_r($collection);

Random number distribution

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;
use loophp\collection\Contract\Operation\Sortable;

$min = 0;
$max = 1000;
$groups = 100;

$randomGenerator = static fn (): array => [random_int($min, $max)];

$distribution = Collection::unfold($randomGenerator)
    ->unwrap()
    ->limit($max)
    ->map(
        static function (int $value) use ($max, $groups): string {
            for ($i = 0; ($max / $groups) > $i; ++$i) {
                if ($i * $groups <= $value && ($i + 1) * $groups >= $value) {
                    return sprintf('%s <= x <= %s', $i * $groups, ($i + 1) * $groups);
                }
            }

            throw new LogicException('Should not happen!');
        }
    )
    ->groupBy(
        static fn ($value, $key) => $value
    )
    ->map(
        static fn (array $value): int => count($value)
    )
    ->sort(
        Sortable::BY_KEYS,
        static function (string $left, string $right): int {
            [$left_min_limit] = explode(' ', $left);
            [$right_min_limit] = explode(' ', $right);

            return $left_min_limit <=> $right_min_limit;
        }
    );

print_r($distribution->all(false));

/*
Array
(
    [0 <= x <= 100] => 108
    [100 <= x <= 200] => 99
    [200 <= x <= 300] => 99
    [300 <= x <= 400] => 96
    [400 <= x <= 500] => 109
    [500 <= x <= 600] => 89
    [600 <= x <= 700] => 92
    [700 <= x <= 800] => 111
    [800 <= x <= 900] => 95
    [900 <= x <= 1000] => 102
)
 */

Parse git log

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;
use loophp\collection\Contract\Collection as CollectionInterface;
use loophp\collection\Contract\Operation\Splitable;

$commandStream = static function (string $command): Generator {
    /** @var resource $fh */
    $fh = popen($command, 'r');

    while (false !== $line = fgets($fh)) {
        yield $line;
    }

    fclose($fh);
};

$buildIfThenElseCallbacks = static fn (string $lineStart): array => [
    static fn ($line): bool => is_string($line) && 0 === mb_strpos($line, $lineStart),
    static function ($line) use ($lineStart): array {
        [, $line] = explode($lineStart, $line);

        return [
            sprintf(
                '%s:%s',
                mb_strtolower(str_replace(':', '', $lineStart)),
                trim($line)
            ),
        ];
    },
];

$c = Collection::fromIterable($commandStream('git log'))
    ->map(
        static fn (string $value): string => trim($value)
    )
    ->compact('', ' ', "\n")
    ->ifThenElse(...$buildIfThenElseCallbacks('commit'))
    ->ifThenElse(...$buildIfThenElseCallbacks('Date:'))
    ->ifThenElse(...$buildIfThenElseCallbacks('Author:'))
    ->ifThenElse(...$buildIfThenElseCallbacks('Merge:'))
    ->ifThenElse(...$buildIfThenElseCallbacks('Signed-off-by:'))
    ->split(
        Splitable::BEFORE,
        static fn ($value): bool => is_array($value) ?
            (1 === preg_match('/^commit:\b[0-9a-f]{5,40}\b/', $value[0])) :
            false
    )
    ->map(
        static fn (array $value): CollectionInterface => Collection::fromIterable($value)
    )
    ->map(
        static fn (CollectionInterface $collection): CollectionInterface => $collection
            ->groupBy(
                static fn ($value): ?string => is_array($value) ? 'headers' : null
            )
            ->groupBy(
                static fn ($value): ?string => is_string($value) ? 'log' : null
            )
            ->ifThenElse(
                static fn ($value, $key): bool => 'headers' === $key,
                static fn ($value, $key): array => Collection::fromIterable($value)
                    ->unwrap()
                    ->associate(
                        static function ($carry, $key, string $value): string {
                            [$key, $line] = explode(':', $value, 2);

                            return $key;
                        },
                        static function ($carry, $key, string $value): string {
                            [$key, $line] = explode(':', $value, 2);

                            return trim($line);
                        }
                    )
                    ->all()
            )
    )
    ->map(
        static fn (CollectionInterface $collection): CollectionInterface => $collection
            ->flatten()
            ->groupBy(
                static function ($value, $key): ?string {
                    if (is_numeric($key)) {
                        return 'log';
                    }

                    return null;
                }
            )
    )
    ->map(
        static fn (CollectionInterface $collection): array => $collection->all()
    )
    ->limit(100);

print_r($c->all());

Collatz conjecture

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

// The Collatz conjecture (https://en.wikipedia.org/wiki/Collatz_conjecture)
$collatz = static fn (int $value): array => 0 === $value % 2
    ? [$value / 2]
    : [$value * 3 + 1];

$collection = Collection::unfold($collatz, [50])
    ->unwrap()
    ->until(static fn (int $number): bool => 1 === $number);

print_r($collection->all()); // [25, 76, 38, 19, 58, 29, 88, 44, 22, 11, 34, 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1]

Read a file

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

$c = Collection::fromFile(__FILE__)
    ->lines()
    ->count();

print_r(sprintf('There are %s lines in this file.', $c)); // There are 13 lines in this file.

Lazy json parsing

<?php

declare(strict_types=1);

namespace App;

require_once __DIR__ . '/../../../vendor/autoload.php';

use JsonMachine\Items;
use loophp\collection\Collection;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Contracts\HttpClient\ChunkInterface;

/**
 * In order to get this working, you will need the library halaxa/json-machine.
 * Use composer to install it in your project.
 *
 * composer require halaxa/json-machine
 *
 * halaxa/json-machine is a lazy JSON parser and it is fully compatible
 * with loophp/collection.
 */

// Parse a local JSON file
$composerJson = __DIR__ . '/../../../composer.json';

$json = Collection::fromIterable(Items::fromFile($composerJson));

foreach ($json as $key => $value);

$remoteFile = 'https://httpbin.org/anything';

// Parse a remote JSON file with Guzzle
$client = new \GuzzleHttp\Client();
$response = $client->request('GET', $remoteFile);
$phpStream = \GuzzleHttp\Psr7\StreamWrapper::getResource($response->getBody());

$json = Collection::fromIterable(\JsonMachine\JsonMachine::fromStream($phpStream));

foreach ($json as $key => $value);

// Parse a remote JSON file with Symfony HTTP client
$client = HttpClient::create();
$response = $client->request('GET', $remoteFile);

$json = Collection::fromIterable(
    JsonMachine::fromIterable(
        Collection::fromIterable($client->stream($response))
            ->map(
                static fn (ChunkInterface $chunk): string => $chunk->getContent()
            )
    )
);

foreach ($json as $key => $value);

Calculate N-Grams

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;

// NGram: https://en.wikipedia.org/wiki/N-gram
$input = 'Hello world';

$ngrams = Collection::fromString($input)
    ->window(2)
    ->drop(2);

print_r($ngrams->all());
/*
[
    0 =>
        [
            0 => 'H',
            1 => 'e',
            2 => 'l',
        ],
    1 =>
        [
            0 => 'e',
            1 => 'l',
            2 => 'l',
        ],
    2 =>
        [
            0 => 'l',
            1 => 'l',
            2 => 'o',
        ],
    3 =>
        [
            0 => 'l',
            1 => 'o',
            2 => ' ',
        ],
    4 =>
        [
            0 => 'o',
            1 => ' ',
            2 => 'w',
        ],
    5 =>
        [
            0 => ' ',
            1 => 'w',
            2 => 'o',
        ],
    6 =>
        [
            0 => 'w',
            1 => 'o',
            2 => 'r',
        ],
    7 =>
        [
            0 => 'o',
            1 => 'r',
            2 => 'l',
        ],
    8 =>
        [
            0 => 'r',
            1 => 'l',
            2 => 'd',
        ],
];
 */

Interactive Fizz-Buzz

<?php

declare(strict_types=1);

include __DIR__ . '/../../../vendor/autoload.php';

use loophp\collection\Collection;
use loophp\collection\Contract\Operation\Splitable;

Collection::fromResource(\STDIN)
    ->split(Splitable::REMOVE, static fn (string $char): bool => "\n" === $char)
    ->map(static fn (array $chars): int => (int) implode('', $chars))
    ->filter(static fn (int $value): bool => 0 <= $value)
    ->map(static fn (int $value): string => sprintf("Result: %s\n", [$value, 'Buzz', 'Fizz', 'Fizz Buzz'][($value % 3 === 0 ? 2 : 0) | ($value % 5 === 0 ? 1 : 0)]))
    ->apply(static fn (string $value): false|int => fwrite(\STDOUT, $value))
    ->all();