Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
232 changes: 0 additions & 232 deletions src/CollectionLogic.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
namespace Noctud\Collection;

use Closure;
use Noctud\Collection\Exception\ConversionException;
use Noctud\Collection\Exception\NoSuchElementException;
use Noctud\Collection\Exception\UnsupportedOperationException;
use Stringable;
use Noctud\Collection\List\ImmutableList;
use Noctud\Collection\List\MutableList;
use Noctud\Collection\Map\ImmutableMap;
Expand Down Expand Up @@ -159,236 +157,6 @@ public function count(): int

// --- Aggregation ---

/** {@inheritDoc} */
public function fold(mixed $initial, Closure $operation): mixed
{
$acc = $initial;
foreach ($this as $v) {
$acc = $operation($acc, $v);
}
return $acc;
}

/** {@inheritDoc} */
public function reduce(Closure $operation): mixed
{
$first = true;
$acc = null;

foreach ($this as $v) {
if ($first) {
$acc = $v;
$first = false;
} else {
$acc = $operation($acc, $v);
}
}

if ($first) {
throw new UnsupportedOperationException();
}

return $acc;
}

/** {@inheritDoc} */
public function reduceOrNull(Closure $operation): mixed
{
try {
return $this->reduce($operation);
} catch (UnsupportedOperationException) {
return null;
}
}

/** {@inheritDoc} */
// @phpstan-ignore conditionalType.subjectNotFound (E is concrete, not a template, in extending fixtures)
public function sum(?Closure $selector = null): int|float
{
$sum = 0;
foreach ($this as $i => $v) {
$sum += $selector !== null ? $selector($v, $i) : $v; // @phpstan-ignore assignOp.invalid
}

return $sum;
}

/** {@inheritDoc} */
public function avg(?Closure $selector = null): float
{
return $this->avgOrNull($selector) ?? throw new UnsupportedOperationException('Cannot compute average of empty collection');
}

/** {@inheritDoc} */
public function avgOrNull(?Closure $selector = null): float|null
{
$sum = 0;
$count = 0;
foreach ($this as $i => $v) {
$sum += $selector !== null ? $selector($v, $i) : $v; // @phpstan-ignore assignOp.invalid
$count++;
}

return $count > 0 ? $sum / $count : null;
}

/** {@inheritDoc} */
public function min(?Closure $selector = null): mixed
{
$minValue = null;
$minElement = null;
$found = false;

foreach ($this as $i => $v) {
$value = $selector !== null ? $selector($v, $i) : $v;
if (!$found || $value < $minValue) {
$minValue = $value;
$minElement = $v;
$found = true;
}
}

if (!$found) {
throw new NoSuchElementException('Collection is empty');
}

return $minElement;
}

/** {@inheritDoc} */
public function minOrNull(?Closure $selector = null): mixed
{
try {
return $this->min($selector);
} catch (NoSuchElementException) {
return null;
}
}

/** {@inheritDoc} */
public function max(?Closure $selector = null): mixed
{
$maxValue = null;
$maxElement = null;
$found = false;

foreach ($this as $i => $v) {
$value = $selector !== null ? $selector($v, $i) : $v;
if (!$found || $value > $maxValue) {
$maxValue = $value;
$maxElement = $v;
$found = true;
}
}

if (!$found) {
throw new NoSuchElementException('Collection is empty');
}

return $maxElement;
}

/** {@inheritDoc} */
public function maxOrNull(?Closure $selector = null): mixed
{
try {
return $this->max($selector);
} catch (NoSuchElementException) {
return null;
}
}

/** {@inheritDoc} */
public function minOf(Closure $selector): mixed
{
$minValue = null;
$found = false;

foreach ($this as $i => $v) {
$value = $selector($v, $i);
if (!$found || $value < $minValue) {
$minValue = $value;
$found = true;
}
}

if (!$found) {
throw new NoSuchElementException('Collection is empty');
}

return $minValue;
}

/** {@inheritDoc} */
public function minOfOrNull(Closure $selector): mixed
{
try {
return $this->minOf($selector);
} catch (NoSuchElementException) {
return null;
}
}

/** {@inheritDoc} */
public function maxOf(Closure $selector): mixed
{
$maxValue = null;
$found = false;

foreach ($this as $i => $v) {
$value = $selector($v, $i);
if (!$found || $value > $maxValue) {
$maxValue = $value;
$found = true;
}
}

if (!$found) {
throw new NoSuchElementException('Collection is empty');
}

return $maxValue;
}

/** {@inheritDoc} */
public function maxOfOrNull(Closure $selector): mixed
{
try {
return $this->maxOf($selector);
} catch (NoSuchElementException) {
return null;
}
}

/** {@inheritDoc} */
public function joinToString(string $separator = ', ', string $prefix = '', string $postfix = '', int $limit = -1, string $truncated = '...', ?Closure $transform = null): string
{
$parts = [];
$i = 0;
foreach ($this as $v) {
if ($limit >= 0 && $i >= $limit) {
$parts[] = $truncated;
break;
}

if ($transform !== null) {
$parts[] = $transform($v, $i);
} elseif (is_scalar($v) || $v === null || $v instanceof Stringable) {
$parts[] = (string) $v;
} else {
throw new ConversionException(sprintf(
'Value of type "%s" at index %d cannot be converted to string. Provide a $transform closure to resolve.',
get_debug_type($v),
$i
));
}

$i++;
}

return $prefix . implode($separator, $parts) . $postfix;
}

/** {@inheritDoc} */
#[NoDiscard]
public function countBy(Closure $keySelector): ImmutableMap
Expand Down
20 changes: 20 additions & 0 deletions src/Exception/UnsupportedOperationException.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,31 @@
namespace Noctud\Collection\Exception;

use LogicException;
use Noctud\Collection\Collection;
use Noctud\Collection\Sequence\Sequence;

/**
* Thrown when a mutating method is called on an immutable collection/map or when a
* requested operation is not valid in the current state (e.g., removing from an empty list).
*/
class UnsupportedOperationException extends LogicException
{
use NamesItsSubject;

/**
* @param Collection<mixed>|Sequence<mixed> $subject
*/
public static function cannotReduceEmptySubject(Collection|Sequence $subject): self
{
return new self(sprintf('Cannot reduce empty %s', lcfirst(self::subjectName($subject))));
}

/**
* @param Collection<mixed>|Sequence<mixed> $subject
*/
public static function cannotAverageEmptySubject(Collection|Sequence $subject): self
{
// Mid-sentence, so the noun is lowercased - which leaves the eager message untouched.
return new self(sprintf('Cannot compute average of empty %s', lcfirst(self::subjectName($subject))));
}
}
Loading
Loading