Skip to content
Open
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
10 changes: 2 additions & 8 deletions src/CollectionLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -598,20 +598,14 @@ public function dropLast(int $n = 1): ImmutableCollection
#[NoDiscard]
public function takeWhile(Closure $predicate): ImmutableCollection
{
$i = 0;
return $this->newCollectionOf(new TakeOperation($this->store)->byPredicate(function ($v) use ($predicate, &$i) {
return $predicate($v, $i++);
}));
return $this->newCollectionOf(new TakeOperation($this->store)->byPredicate($predicate));
}

/** {@inheritDoc} */
#[NoDiscard]
public function dropWhile(Closure $predicate): ImmutableCollection
{
$i = 0;
return $this->newCollectionOf(new DropOperation($this->store)->byPredicate(function ($v) use ($predicate, &$i) {
return $predicate($v, $i++);
}));
return $this->newCollectionOf(new DropOperation($this->store)->byPredicate($predicate));
}

/** {@inheritDoc} */
Expand Down
10 changes: 2 additions & 8 deletions src/List/ListLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -299,10 +299,7 @@ public function dropLast(int $n = 1): ImmutableList
#[NoDiscard]
public function takeWhile(Closure $predicate): ImmutableList
{
$i = 0;
return $this->newCollectionOf(new TakeOperation($this->store)->byPredicate(function ($v) use ($predicate, &$i) {
return $predicate($v, $i++);
}));
return $this->newCollectionOf(new TakeOperation($this->store)->byPredicate($predicate));
}

/**
Expand All @@ -313,10 +310,7 @@ public function takeWhile(Closure $predicate): ImmutableList
#[NoDiscard]
public function dropWhile(Closure $predicate): ImmutableList
{
$i = 0;
return $this->newCollectionOf(new DropOperation($this->store)->byPredicate(function ($v) use ($predicate, &$i) {
return $predicate($v, $i++);
}));
return $this->newCollectionOf(new DropOperation($this->store)->byPredicate($predicate));
}

/**
Expand Down
6 changes: 3 additions & 3 deletions src/Operation/DropOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,14 @@ public function last(int $n): array
}

/**
* @param callable(V):bool $predicate
* @param callable(V, int):bool $predicate
* @return Generator<V>
*/
public function byPredicate(callable $predicate): Generator
{
$dropping = true;
foreach ($this->data as $v) {
if ($dropping && $predicate($v)) {
foreach ($this->data as $i => $v) {
if ($dropping && $predicate($v, $i)) {
continue;
}
$dropping = false;
Expand Down
11 changes: 6 additions & 5 deletions src/Operation/TakeOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ public function first(int $n): Generator

$i = 0;
foreach ($this->data as $v) {
if ($i++ >= $n) {
yield $v;

if (++$i >= $n) {
break;
}
yield $v;
}
}

Expand All @@ -53,13 +54,13 @@ public function last(int $n): array
}

/**
* @param callable(V):bool $predicate
* @param callable(V, int):bool $predicate
* @return Generator<V>
*/
public function byPredicate(callable $predicate): Generator
{
foreach ($this->data as $v) {
if (!$predicate($v)) {
foreach ($this->data as $i => $v) {
if (!$predicate($v, $i)) {
break;
}
yield $v;
Expand Down
51 changes: 43 additions & 8 deletions src/Operation/ZipOperation.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@

namespace Noctud\Collection\Operation;

use ArrayIterator;
use Generator;
use Traversable;
use Iterator;
use IteratorIterator;

/**
* @internal
Expand All @@ -26,17 +28,50 @@ final class ZipOperation extends AbstractOperation
*/
public function with(iterable $other): Generator
{
$otherArray = $other instanceof Traversable ? iterator_to_array($other, false) : array_values($other);
$otherCount = count($otherArray);
$i = 0;
$left = self::cursor($this->data);
$right = self::cursor($other);

foreach ($this->data as $v) {
if ($i >= $otherCount) {
while ($left->valid() && $right->valid()) {
yield [$left->current(), $right->current()];

// Advancing the other side only once this one still has an element spares it a
// pull that would be thrown away whenever this side is the shorter one.
$left->next();

if (!$left->valid()) {
break;
}

yield [$v, $otherArray[$i]];
$i++;
$right->next();
}
}

/**
* Positioned cursor over any iterable, so both sides can be walked in lockstep without
* either being buffered.
*
* An Iterator is used as is and never rewound: it may be a cursor that has already
* started, and rewinding a running Generator throws.
*
* @template T
* @param iterable<T> $iterable
* @return Iterator<T>
*/
private static function cursor(iterable $iterable): Iterator
{
if (is_array($iterable)) {
return new ArrayIterator($iterable);
}

if ($iterable instanceof Iterator) {
return $iterable;
}

// An IteratorAggregate only hands out its iterator on rewind(): before that the
// wrapper has no position at all, and valid() answers false.
$cursor = new IteratorIterator($iterable);
$cursor->rewind();

return $cursor;
}
}
144 changes: 144 additions & 0 deletions src/Sequence/Sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ interface Sequence extends IteratorAggregate
#[NoDiscard]
public function filter(Closure $predicate): Sequence;

/**
* Filter non-null elements.
*
* @return Sequence<(E is null ? never : E)>
*/
#[NoDiscard]
public function filterNotNull(): Sequence;

/**
* Filter elements that are instances of the given class or interface.
*
* @template T
* @param class-string<T> $type
* @return Sequence<T>
*/
#[NoDiscard]
public function filterInstanceOf(string $type): Sequence;

/**
* Map elements to a new sequence.
* The transform receives the element and optionally the index.
Expand All @@ -66,6 +84,132 @@ public function filter(Closure $predicate): Sequence;
#[NoDiscard]
public function map(Closure $transform): Sequence;

/**
* Transforms each element using the given function and excludes null results.
* Combines map and filterNotNull in a single operation.
*
* @template R
* @param Closure(E, int):(R|null) $transform
* @return Sequence<R>
*/
#[NoDiscard]
public function mapNotNull(Closure $transform): Sequence;

/**
* Flat map elements to a new sequence.
* Each iterable returned by the transform is consumed lazily, element by element.
*
* @template R
* @param Closure(E, int):iterable<R> $transform
* @return Sequence<R>
*/
#[NoDiscard]
public function flatMap(Closure $transform): Sequence;

/**
* Flatten a sequence of iterables into a single sequence.
* Iterable elements are flattened one level; non-iterable elements are kept as-is.
* The array{} in value-of keeps the type resolvable when E is never (empty sequences).
*
* @return Sequence<(E is iterable<mixed> ? value-of<E|array{}> : E)>
*/
#[NoDiscard]
public function flatten(): Sequence;

/**
* Take the first N elements.
* The source is not pulled any further once N elements have been yielded.
*
* @param non-negative-int $n
* @return Sequence<E>
*/
#[NoDiscard]
public function takeFirst(int $n = 1): Sequence;

/**
* Drops the first N elements.
*
* @param non-negative-int $n
* @return Sequence<E>
*/
#[NoDiscard]
public function dropFirst(int $n = 1): Sequence;

/**
* Takes elements while the predicate is true.
* The source is not pulled any further once the predicate has returned false.
*
* @param Closure(E, int):bool $predicate
* @return Sequence<E>
*/
#[NoDiscard]
public function takeWhile(Closure $predicate): Sequence;

/**
* Drops elements while the predicate is true, then returns the rest.
*
* @param Closure(E, int):bool $predicate
* @return Sequence<E>
*/
#[NoDiscard]
public function dropWhile(Closure $predicate): Sequence;

/**
* Distinct elements by identity.
* Elements already seen during the current pass are skipped, so the memory held grows
* with the number of distinct elements.
*
* @return Sequence<E>
*/
#[NoDiscard]
public function distinct(): Sequence;

/**
* Distinct elements by selector.
*
* @template K
* @param Closure(E, int):K $selector
* @return Sequence<E>
*/
#[NoDiscard]
public function distinctBy(Closure $selector): Sequence;

/**
* Combines this sequence with another iterable by pairing elements at the same position.
* The resulting sequence has the length of the shorter input.
*
* @template U
* @param iterable<U> $other
* @return Sequence<array{E, U}>
*/
#[NoDiscard]
public function zip(iterable $other): Sequence;

/**
* Returns a sequence of pairs of each two adjacent elements in this sequence.
* If the sequence has fewer than two elements, yields nothing.
*
* @return Sequence<array{E, E}>
*/
#[NoDiscard]
public function zipWithNext(): Sequence;

// --- Iteration ---

/**
* Returns a sequence applying the given action to each element as it goes through,
* then yielding the element unchanged.
*
* The lazy, chainable counterpart of forEach: the action runs once per element and per
* pass, while the element flows through the pipeline - nothing happens before a
* terminal operation pulls.
*
* @param Closure(E, int):void $action
* @return Sequence<E>
*/
#[NoDiscard]
public function onEach(Closure $action): Sequence;

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm introducing onEach(): Sequence before forEach(): void

there is currently no BC break


// --- Conversion ---

/**
Expand Down
Loading
Loading