diff --git a/spec/loophp/collection/Iterator/RandomIteratorSpec.php b/spec/loophp/collection/Iterator/RandomIteratorSpec.php index 83cf5d9bb..8eb66d0ee 100644 --- a/spec/loophp/collection/Iterator/RandomIteratorSpec.php +++ b/spec/loophp/collection/Iterator/RandomIteratorSpec.php @@ -5,11 +5,75 @@ namespace spec\loophp\collection\Iterator; use ArrayIterator; +use Exception; use loophp\collection\Iterator\RandomIterator; use PhpSpec\ObjectBehavior; class RandomIteratorSpec extends ObjectBehavior { + public function it_can_build_an_iterator_with_a_random_seed() + { + $input = new ArrayIterator(range('a', 'z')); + $seed = 123; + + $this->beConstructedWith( + $input, + $seed + ); + + $expected = [ + 2 => 'c', + 4 => 'e', + 22 => 'w', + 21 => 'v', + 20 => 'u', + 14 => 'o', + 5 => 'f', + 17 => 'r', + 25 => 'z', + 24 => 'y', + 10 => 'k', + 13 => 'n', + 23 => 'x', + 15 => 'p', + 8 => 'i', + 11 => 'l', + 0 => 'a', + 7 => 'h', + 19 => 't', + 9 => 'j', + 3 => 'd', + 16 => 'q', + 18 => 's', + 1 => 'b', + 12 => 'm', + 6 => 'g', + ]; + + if (iterator_to_array($this->getWrappedObject()) !== $expected) { + throw new Exception('Iterator is not equal to the expected array.'); + } + + $iterator1 = new RandomIterator($input, $seed); + $iterator2 = new RandomIterator($input, $seed + $seed); + + if (iterator_to_array($iterator1) === iterator_to_array($iterator2)) { + throw new Exception('Iterator1 is equal to Iterator2'); + } + } + + public function it_can_build_an_iterator_without_a_random_seed() + { + $input = new ArrayIterator(range('a', 'z')); + $this->beConstructedWith($input); + + $iterator1 = new RandomIterator($input); + + if (iterator_to_array($iterator1) === iterator_to_array($this->getWrappedObject())) { + throw new Exception('Iterator1 is equal to Iterator2'); + } + } + public function it_can_get_the_innerIterator() { $this->getInnerIterator()->shouldBeAnInstanceOf(ArrayIterator::class); @@ -19,7 +83,7 @@ public function it_can_rewind() { $iterator = new ArrayIterator(['a']); - $this->beConstructedWith($iterator); + $this->beConstructedWith($iterator, 1); $this ->valid() @@ -56,6 +120,6 @@ public function let() { $iterator = new ArrayIterator(range('a', 'c')); - $this->beConstructedWith($iterator); + $this->beConstructedWith($iterator, 1); } } diff --git a/src/Collection.php b/src/Collection.php index e3e88be8b..bea5a4a9b 100644 --- a/src/Collection.php +++ b/src/Collection.php @@ -115,6 +115,7 @@ use const INF; use const PHP_INT_MAX; +use const PHP_INT_MIN; /** * @psalm-template TKey @@ -588,9 +589,13 @@ public function product(iterable ...$iterables): CollectionInterface return new self(Product::of()(...$iterables), $this->getIterator()); } - public function random(int $size = 1): CollectionInterface + public function random(int $size = 1, ?int $seed = null): CollectionInterface { - return new self(Random::of()($size), $this->getIterator()); + if (null === $seed) { + $seed = random_int(PHP_INT_MIN, PHP_INT_MAX); + } + + return new self(Random::of()($seed)($size), $this->getIterator()); } public static function range(float $start = 0.0, float $end = INF, float $step = 1.0): CollectionInterface @@ -643,9 +648,13 @@ public function scanRight1(callable $callback): CollectionInterface return new self(ScanRight1::of()($callback), $this->getIterator()); } - public function shuffle(): CollectionInterface + public function shuffle(?int $seed = null): CollectionInterface { - return new self(Shuffle::of(), $this->getIterator()); + if (null === $seed) { + $seed = random_int(PHP_INT_MIN, PHP_INT_MAX); + } + + return new self(Shuffle::of()($seed), $this->getIterator()); } public function since(callable ...$callbacks): CollectionInterface diff --git a/src/Iterator/RandomIterator.php b/src/Iterator/RandomIterator.php index 416753046..e32634dec 100644 --- a/src/Iterator/RandomIterator.php +++ b/src/Iterator/RandomIterator.php @@ -7,6 +7,11 @@ use ArrayIterator; use Iterator; +use function array_slice; + +use const PHP_INT_MAX; +use const PHP_INT_MIN; + /** * @psalm-template TKey * @psalm-template TKey of array-key @@ -36,12 +41,13 @@ final class RandomIterator extends ProxyIterator /** * @psalm-param Iterator $iterator */ - public function __construct(Iterator $iterator) + public function __construct(Iterator $iterator, ?int $seed = null) { $this->iterator = $iterator; + $this->seed = $seed ?? random_int(PHP_INT_MIN, PHP_INT_MAX); $this->wrappedIterator = $this->buildArrayIterator($iterator); $this->indexes = array_keys($this->wrappedIterator->getArrayCopy()); - $this->key = array_rand($this->indexes); + $this->key = current($this->customArrayRand($this->indexes, 1, $this->seed)); } public function current() @@ -63,7 +69,7 @@ public function next(): void unset($this->indexes[$this->key]); if ($this->valid()) { - $this->key = array_rand($this->indexes); + $this->key = current($this->customArrayRand($this->indexes, 1, $this->seed)); } } @@ -94,4 +100,13 @@ private function buildArrayIterator(Iterator $iterator): ArrayIterator return $arrayIterator; } + + private function customArrayRand(array $array, int $num, int $seed): array + { + mt_srand($seed); + shuffle($array); + mt_srand(); + + return array_slice($array, 0, $num); + } } diff --git a/src/Operation/Random.php b/src/Operation/Random.php index a9645c3bf..18d74e290 100644 --- a/src/Operation/Random.php +++ b/src/Operation/Random.php @@ -16,23 +16,25 @@ final class Random extends AbstractOperation { /** - * @psalm-return Closure(int): Closure(Iterator): Generator + * @psalm-return Closure(int): Closure(int): Closure(Iterator): Generator */ public function __invoke(): Closure { return - /** - * @psalm-return Closure(Iterator): Generator - */ - static function (int $size): Closure { - /** @psalm-var Closure(Iterator): Generator $pipe */ - $pipe = Pipe::of()( - Shuffle::of(), - Limit::of()($size)(0) - ); + static function (int $seed): Closure { + /** + * @psalm-return Closure(Iterator): Generator + */ + return static function (int $size) use ($seed): Closure { + /** @psalm-var Closure(Iterator): Generator $pipe */ + $pipe = Pipe::of()( + Shuffle::of()($seed), + Limit::of()($size)(0) + ); - // Point free style. - return $pipe; + // Point free style. + return $pipe; + }; }; } } diff --git a/src/Operation/Shuffle.php b/src/Operation/Shuffle.php index b5ea78b75..507722212 100644 --- a/src/Operation/Shuffle.php +++ b/src/Operation/Shuffle.php @@ -17,16 +17,22 @@ final class Shuffle extends AbstractOperation { /** - * @psalm-return Closure(Iterator): Generator + * @psalm-return Closure(int): Closure(Iterator): Generator */ public function __invoke(): Closure { return /** - * @psalm-param Iterator $iterator - * - * @psalm-return Generator + * @return Closure(Iterator): Generator */ - static fn (Iterator $iterator): Generator => yield from new RandomIterator($iterator); + static function (int $seed): Closure { + return + /** + * @psalm-param Iterator $iterator + * + * @psalm-return Generator + */ + static fn (Iterator $iterator): Generator => yield from new RandomIterator($iterator, $seed); + }; } }