Skip to content
Merged
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
50 changes: 0 additions & 50 deletions src/CollectionLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,48 +139,12 @@ public function containsAll(iterable $elements): bool
return true;
}

/** {@inheritDoc} */
public function all(Closure $predicate): bool
{
foreach ($this as $i => $v) {
if (!$predicate($v, $i)) {
return false;
}
}

return true;
}

/** {@inheritDoc} */
public function any(Closure $predicate): bool
{
foreach ($this as $i => $v) {
if ($predicate($v, $i)) {
return true;
}
}

return false;
}

/** {@inheritDoc} */
public function none(Closure $predicate): bool
{
return !$this->any($predicate);
}

/** {@inheritDoc} */
public function isEmpty(): bool
{
return $this->store->isEmpty();
}

/** {@inheritDoc} */
public function isNotEmpty(): bool
{
return !$this->isEmpty();
}

/**
* {@inheritDoc}
* @return int<0, max>
Expand All @@ -193,20 +157,6 @@ public function count(): int
return $storeCount;
}

/** {@inheritDoc} */
public function countWhere(Closure $predicate): int
{
/** @var int<0, max> $count */
$count = 0;
foreach ($this as $i => $v) {
if ($predicate($v, $i)) {
$count++;
}
}

return $count;
}

// --- Aggregation ---

/** {@inheritDoc} */
Expand Down
61 changes: 58 additions & 3 deletions src/IterableTerminalsLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
* Terminal operations implemented by walking $this and nothing else, shared by the eager
* Collection side and the lazy Sequence side.
*
* A body belongs here only if it is identical for both, which excludes the family the eager side
* answers from its store in O(1) (first/last/contains/count). A message naming the subject is no
* longer a reason to split: the exception derives that noun from $this.
* A body belongs here only if it is identical for both, which excludes two families: the one the
* eager side answers from its store in O(1) (first/last/isEmpty/contains/count), and containsAll,
* whose one-lookup-per-value shape would cost a sequence one pass per value. A message naming the
* subject is no longer a reason to split: the exception derives that noun from $this.
*
* Consumers declare the contract, so the PHPDoc here is {@inheritDoc}: it resolves against
* Collection<E> or Sequence<E> depending on who uses the trait.
Expand All @@ -29,6 +30,8 @@
*/
trait IterableTerminalsLogic
{
// --- Element Access ---

/** {@inheritDoc} */
public function single()
{
Expand Down Expand Up @@ -97,4 +100,56 @@ public function expect(Closure $predicate)

throw new NoSuchElementException('No element matching the predicate was found');
}

// --- Querying ---

/** {@inheritDoc} */
public function isNotEmpty(): bool
{
return !$this->isEmpty();
}

/** {@inheritDoc} */
public function all(Closure $predicate): bool
{
foreach ($this as $i => $v) {
if (!$predicate($v, $i)) {
return false;
}
}

return true;
}

/** {@inheritDoc} */
public function any(Closure $predicate): bool
{
foreach ($this as $i => $v) {
if ($predicate($v, $i)) {
return true;
}
}

return false;
}

/** {@inheritDoc} */
public function none(Closure $predicate): bool
{
return !$this->any($predicate);
}

/** {@inheritDoc} */
public function countWhere(Closure $predicate): int
{
/** @var int<0, max> $count */
$count = 0;
foreach ($this as $i => $v) {
if ($predicate($v, $i)) {
$count++;
}
}

return $count;
}
}
97 changes: 93 additions & 4 deletions src/Sequence/Sequence.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
*
* Keys are positional: every pass yields fresh 0..n keys, whatever the source yields.
*
* Deliberately neither Countable (counting would silently consume a pass; native
* count($seq) is a TypeError by design) nor JsonSerializable
* (json_encode would be a hidden materialization) - materialize explicitly with
* toList() or toArray() instead.
* Deliberately neither Countable (native count($seq) stays a TypeError by design;
* $seq->count() exists as an explicit O(n) terminal that drains a pass) nor
* JsonSerializable (json_encode would be a hidden materialization) - materialize
* explicitly with toList() or toArray() instead.
*
* @template E
* @extends IteratorAggregate<int, E>
Expand Down Expand Up @@ -329,6 +329,95 @@ public function find(Closure $predicate): mixed;
*/
public function expect(Closure $predicate);

// --- Querying ---

/**
* Whether the sequence does not contain any elements. Pulls exactly one element.
*
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
Comment thread
nikophil marked this conversation as resolved.
*/
public function isEmpty(): bool;
Comment thread
nikophil marked this conversation as resolved.

/**
* Whether the sequence contains at least one element. Pulls exactly one element.
*
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function isNotEmpty(): bool;

/**
* Whether the sequence contains a value (strict comparison).
* Stops pulling at the first match.
*
* @param E $element
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function contains(mixed $element): bool;

/**
* Whether the sequence contains all the provided values.
* Walks the sequence once, stopping as soon as none is left to look for.
*
* @param iterable<E> $elements
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function containsAll(iterable $elements): bool;

/**
* Returns true if all elements match the predicate.
* Stops pulling at the first element that does not.
*
* @param Closure(E, int):bool $predicate
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function all(Closure $predicate): bool;

/**
* Returns true if any element matches the predicate.
* Stops pulling at the first match.
*
* @param Closure(E, int):bool $predicate
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function any(Closure $predicate): bool;

/**
* Returns true if no element matches the predicate.
* Stops pulling at the first match.
*
* @param Closure(E, int):bool $predicate
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function none(Closure $predicate): bool;

/**
* Returns the number of elements in the sequence, draining it.
* The O(n) counterpart of a Collection's O(1) count: a sequence has no length to read, only
* elements to pull, which is why the cost has to be asked for explicitly.
*
* @return int<0, max>
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function count(): int;
Comment thread
nikophil marked this conversation as resolved.

/**
* Returns the number of elements matching the predicate, draining the sequence.
*
* @param Closure(E, int):bool $predicate
* @return int<0, max>
* @throws NonReplayableSourceException If a non-replayable source has already been consumed
* @throws InvalidSequenceSourceException If a Closure source returns a non-iterable
*/
public function countWhere(Closure $predicate): int;

// --- Conversion ---

/**
Expand Down
88 changes: 88 additions & 0 deletions src/Sequence/SequenceLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,94 @@ public function lastOrNull(): mixed
return $result;
}

// --- Querying ---

/**
* {@inheritDoc}
*
* The eager side reads its store; here a single element settles the question, so nothing
* beyond the first is pulled.
*/
public function isEmpty(): bool
{
foreach ($this as $ignored) { // phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable
return false;
}

return true;
}

/**
* {@inheritDoc}
*
* No hash index to ask, so this is the linear scan the eager side avoids - it does stop at
* the first match.
*/
public function contains(mixed $element): bool
{
foreach ($this as $v) {
if ($v === $element) {
return true;
}
}

return false;
}

/**
* {@inheritDoc}
*
* Unlike its Collection counterpart this cannot look each value up in turn - that would cost
* one pass per value, which a single-pass source cannot give. So the values still being looked
* for are carried through a single walk, and dropped as they are met.
*/
public function containsAll(iterable $elements): bool
{
$missing = [];
foreach ($elements as $element) {
$missing[] = $element;
}

if ($missing === []) {
return true;
}

foreach ($this as $v) {
if (!in_array($v, $missing, true)) {
continue;
}

// Every equal entry drops, not just the first: containsAll([1, 1]) asks whether 1 is
// there, not whether it is there twice - the same answer the eager side gives.
$missing = array_filter($missing, static fn (mixed $wanted): bool => $wanted !== $v);

if ($missing === []) {
return true;
}
}

return false;
}

/**
* {@inheritDoc}
*
* O(n) where the eager side is O(1), and draining: the count is only known once the source
* runs out.
*
* @return int<0, max>
*/
public function count(): int
{
/** @var int<0, max> $count */
$count = 0;
foreach ($this as $_) { // phpcs:ignore SlevomatCodingStandard.Variables.UnusedVariable.UnusedVariable
$count++;
}

return $count;
}

// --- Conversion ---

/** {@inheritDoc} */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
use function Noctud\Collection\listOf;
use function Noctud\Collection\sequenceOf;

final class SequenceTerminalTest extends TestCase
final class SequenceAccessTest extends TestCase
{
#[Test]
public function first_returns_the_first_element(): void
Expand Down
Loading
Loading