Skip to content

Commit

Permalink
refactor: Update cross product operation in point free style. (#177)
Browse files Browse the repository at this point in the history
* refactor: Update `Product` operation in point free style.

* refactor: Update `Productable` interface.

* tests: Update tests accordingly.

* tests: Add basic SA tests.
  • Loading branch information
drupol authored Aug 18, 2021
1 parent 74d3069 commit 0fe81c7
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 54 deletions.
51 changes: 32 additions & 19 deletions spec/loophp/collection/CollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,14 @@ public function it_can_be_constructed_from_a_stream(): void
$this::fromResource($stream)
->shouldThrow(InvalidArgumentException::class)
->during('all');

$string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
$stream = fopen('data://text/plain,' . $string, 'rb');

$this::fromResource($stream)
->drop(1)
->count()
->shouldReturn(55);
}

public function it_can_be_constructed_from_a_string(): void
Expand Down Expand Up @@ -944,25 +952,6 @@ public function name(): string
->shouldIterateAs([$cat1, $cat2, $cat3]);
}

public function it_can_do_the_cartesian_product(): void
{
$this::fromIterable(range('A', 'C'))
->product()
->shouldIterateAs([0 => ['A'], 1 => ['B'], 2 => ['C']]);

$this::fromIterable(range('A', 'C'))
->product([1, 2])
->shouldIterateAs([0 => ['A', 1], 1 => ['A', 2], 2 => ['B', 1], 3 => ['B', 2], 4 => ['C', 1], 5 => ['C', 2]]);

$string = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
$stream = fopen('data://text/plain,' . $string, 'rb');

$this::fromResource($stream)
->drop(1)
->count()
->shouldReturn(55);
}

public function it_can_drop(): void
{
$this::fromIterable(range('A', 'F'))
Expand Down Expand Up @@ -2713,6 +2702,30 @@ public function it_can_prepend(): void
->shouldIterateAs($generator());
}

public function it_can_product(): void
{
$this::fromIterable(range('A', 'C'))
->product()
->shouldIterateAs([0 => ['A'], 1 => ['B'], 2 => ['C']]);

$this::fromIterable(range('A', 'C'))
->product([1, 2], [3, 4])
->shouldIterateAs([
['A', 1, 3],
['A', 1, 4],
['A', 2, 3],
['A', 2, 4],
['B', 1, 3],
['B', 1, 4],
['B', 2, 3],
['B', 2, 4],
['C', 1, 3],
['C', 1, 4],
['C', 2, 3],
['C', 2, 4],
]);
}

public function it_can_random(): void
{
$input = range('a', 'z');
Expand Down
7 changes: 5 additions & 2 deletions src/Contract/Operation/Productable.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,12 @@ interface Productable
*
* @see https://loophp-collection.readthedocs.io/en/stable/pages/api.html#product
*
* @param iterable<mixed> ...$iterables
* @template UKey
* @template U
*
* @return Collection<TKey, T>
* @param iterable<UKey, U> ...$iterables
*
* @return Collection<TKey, list<T|U>>
*/
public function product(iterable ...$iterables): Collection;
}
99 changes: 66 additions & 33 deletions src/Operation/Product.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@

namespace loophp\collection\Operation;

use ArrayIterator;
use Closure;
use Generator;
use Iterator;

use function count;
use loophp\collection\Iterator\IterableIterator;

/**
* @immutable
Expand All @@ -25,46 +25,79 @@ final class Product extends AbstractOperation
{
/**
* @pure
*
* @template UKey
* @template U
*/
public function __invoke(): Closure
{
return
/**
* @param iterable<TKey, T> ...$iterables
* @param iterable<UKey, U> ...$iterables
*
* @return Closure(Iterator<TKey, T>): Generator<int, array<int, T>>
* @return Closure(Iterator<TKey, T>): Generator<int, list<T|U>>
*/
static fn (iterable ...$iterables): Closure =>
/**
* @param Iterator<TKey, T> $iterator
*
* @return Generator<int, array<int, T>>
*/
static function (Iterator $iterator) use ($iterables): Iterator {
/** @var Closure(iterable<TKey, T>...): Generator<int, array<int, T>> $cartesian */
$cartesian =
static function (iterable ...$iterables): Closure {
/** @var Closure(Iterator<TKey, T>): Generator<int, list<T|U>> $pipe */
$pipe = Pipe::of()(
(
/**
* @param iterable<TKey, T> ...$iterables
*
* @return Generator<int, array<int, T>>
* @param list<Iterator<UKey, U>> $iterables
*/
static function (iterable ...$iterables) use (&$cartesian): Generator {
$iterable = array_pop($iterables);

if (null === $iterable) {
return yield [];
}

// @todo Find better algo, without recursion.
/** @var array<int, T> $item */
foreach ($cartesian(...$iterables) as $item) {
foreach ($iterable as $value) {
yield $item + [count($item) => $value];
}
}
};
static fn (array $iterables): Closure =>
/**
* @param Iterator<TKey, T> $iterator
*/
static fn (Iterator $iterator): Generator => (
/**
* @param Closure(Iterator<TKey, T>): (Closure(Iterator<UKey, U>): Generator<list<T|U>>) $f
*/
static fn (Closure $f): Closure => (new FoldLeft())()(
/**
* @param Iterator<UKey, U> $a
* @param Iterator<TKey, T> $x
*/
static fn (Iterator $a, Iterator $x): Generator => $f($x)($a)
)
)(
/**
* @param (Iterator<TKey, T>|Iterator<UKey, U>) $xs
*/
static fn (Iterator $xs): Closure =>
/**
* @param Iterator<int, list<T>> $as
*/
static fn (Iterator $as): Generator => FlatMap::of()(
/**
* @param list<T> $a
*/
static fn (array $a): Generator => FlatMap::of()(
/**
* @param T|U $x
*
* @return Generator<int, list<T|U>>
*/
static fn ($x): Generator => yield [...$a, $x]
)($xs)
)($as)
)(new ArrayIterator([[]]))(new ArrayIterator([$iterator, ...$iterables]))
)(
array_map(
/**
* @param iterable<UKey, U> $iterable
*
* @return Iterator<UKey, U>
*/
static fn (iterable $iterable): Iterator => new IterableIterator($iterable),
$iterables
)
),
((new Unwrap())()),
((new Normalize())())
);

return $cartesian($iterator, ...$iterables);
};
// Point free style.
return $pipe;
};
}
}
25 changes: 25 additions & 0 deletions tests/static-analysis/product.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

/**
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/

declare(strict_types=1);

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

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

/**
* @param CollectionInterface<int, list<int|string>> $collection
*/
function product_checkList(CollectionInterface $collection): void
{
}

$input = range('a', 'e');
$productWith = range(1, 5);

product_checkList(Collection::fromIterable($input)->product($productWith));

0 comments on commit 0fe81c7

Please sign in to comment.