Skip to content

Latest commit

 

History

History
674 lines (475 loc) · 17.9 KB

File metadata and controls

674 lines (475 loc) · 17.9 KB

Reduce

Back to main README

Tools for reducing iterable collections to single values.


To Average

Reduces to the mean average.

Returns null if collection is empty.

Reduce::toAverage(iterable $data): float

use IterTools\Reduce;

$grades = [100, 90, 95, 85, 94];

$finalGrade = Reduce::toAverage($numbers);
// 92.8

To Count

Reduces iterable to its length.

Reduce::toCount(iterable $data): int

use IterTools\Reduce;

$someIterable = ImportantThing::getCollectionAsIterable();

$length = Reduce::toCount($someIterable);
// 3

To Count By

Reduces iterable to an array of counts keyed by the value returned from the key function.

Reduce::toCountBy(iterable $data, callable $keyFunc): array

The key function must return an int or string (the only valid array-key types). Any other return type throws a \TypeError naming the offending type.

Note: PHP arrays coerce numeric-string keys to int — a key function that returns the string "1" and one that returns the int 1 collapse into a single int key 1 with the combined count.

use IterTools\Reduce;

$words = ['apple', 'pear', 'banana', 'kiwi', 'plum'];

$counts = Reduce::toCountBy($words, fn ($word) => \strlen($word));
// [5 => 1, 4 => 3, 6 => 1]

To First

Reduces iterable to its first element.

Reduce::toFirst(iterable $data): mixed

Throws \LengthException if collection is empty.

use IterTools\Reduce;

$medals = ['gold', 'silver', 'bronze'];

$first = Reduce::toFirst($medals);
// gold

To First And Last

Reduces iterable to its first and last elements.

Reduce::toFirstAndLast(iterable $data): array{mixed, mixed}

Throws \LengthException if collection is empty.

use IterTools\Reduce;

$weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday'];

$firstAndLast = Reduce::toFirstAndLast($weekdays);
// [Monday, Friday]

To First Match

Reduces iterable to its first element matching the predicate.

Reduce::toFirstMatch(iterable $data, callable $predicate, mixed $default = null): mixed

  • Predicate return value is coerced via (bool) cast.
  • Short-circuits on the first match — the iterable is not fully consumed.
  • Returns $default (null by default) if no element matches.
use IterTools\Reduce;

$numbers = [1, 3, 5, 6, 7, 8];

$firstEven = Reduce::toFirstMatch($numbers, fn (int $n) => $n % 2 === 0);
// 6

$firstNegative = Reduce::toFirstMatch($numbers, fn (int $n) => $n < 0, -1);
// -1

To First Match Index

Reduces iterable to the zero-based position of the first element matching the predicate.

Reduce::toFirstMatchIndex(iterable $data, callable $predicate, mixed $default = null): mixed

  • Predicate return value is coerced via (bool) cast.
  • Short-circuits on the first match — the iterable is not fully consumed.
  • Returns $default (null by default) if no element matches.
  • Position is always counted from the start of iteration, regardless of source keys.
use IterTools\Reduce;

$numbers = [10, 20, 30, 40];

$firstOver25Index = Reduce::toFirstMatchIndex($numbers, fn (int $n) => $n > 25);
// 2
use IterTools\Reduce;

// Early-exit search: a generator that would throw on the item after the match
// is never advanced past the matching position.
$ids = (function (): \Generator {
    yield 1;
    yield 2;
    yield 3;
    throw new \RuntimeException('iterator advanced past match');
})();

$index = Reduce::toFirstMatchIndex($ids, fn (int $n) => $n === 2);
// 1

To First Match Key

Reduces iterable to the source key of the first element matching the predicate.

Reduce::toFirstMatchKey(iterable $data, callable $predicate, mixed $default = null): mixed

  • Predicate return value is coerced via (bool) cast.
  • Short-circuits on the first match — the iterable is not fully consumed.
  • Returns $default (null by default) if no element matches.
  • Preserves the source key (string for associative input, int for list-shape input).
use IterTools\Reduce;

$users = ['alice' => 12, 'bob' => 17, 'carol' => 22, 'dan' => 30];

$firstAdultName = Reduce::toFirstMatchKey($users, fn (int $age) => $age >= 18);
// 'carol'
use IterTools\Reduce;

$prices = ['usd' => 9.99, 'eur' => 8.49, 'jpy' => 1499.0];

$firstExpensiveCurrency = Reduce::toFirstMatchKey(
    $prices,
    fn (float $p) => $p > 1000,
    'none'
);
// 'jpy'

To Last

Reduces iterable to its last element.

Reduce::toLast(iterable $data): mixed

Throws \LengthException if collection is empty.

use IterTools\Reduce;

$gnomesThreePhasePlan = ['Collect underpants', '?', 'Profit'];

$lastPhase = Reduce::toLast($gnomesThreePhasePlan);
// Profit

To Last Match

Reduces iterable to the last element matching the predicate.

Reduce::toLastMatch(iterable $data, callable $predicate, mixed $default = null): mixed

  • Predicate return value is coerced via (bool) cast.
  • Consumes the entire iterable (no short-circuit possible).
  • Returns $default (null by default) if no element matches.
use IterTools\Reduce;

$numbers = [1, 3, 5, 6, 7, 8, 9];

$lastEven = Reduce::toLastMatch($numbers, fn (int $n) => $n % 2 === 0);
// 8

$lastNegative = Reduce::toLastMatch($numbers, fn (int $n) => $n < 0, 'none');
// 'none'

To Last Match Index

Reduces iterable to the zero-based position of the last element matching the predicate.

Reduce::toLastMatchIndex(iterable $data, callable $predicate, mixed $default = null): mixed

  • Predicate return value is coerced via (bool) cast.
  • Consumes the entire iterable.
  • Returns $default (null by default) if no element matches.
  • For associative input, returns the zero-based position rather than the source key.
use IterTools\Reduce;

$numbers = [10, 20, 30, 40, 5];

$lastOver25Index = Reduce::toLastMatchIndex($numbers, fn (int $n) => $n > 25);
// 3

To Last Match Key

Reduces iterable to the source key of the last element matching the predicate.

Reduce::toLastMatchKey(iterable $data, callable $predicate, mixed $default = null): mixed

  • Predicate return value is coerced via (bool) cast.
  • Consumes the entire iterable.
  • Returns $default (null by default) if no element matches.
  • Preserves the source key (string for associative input, int for list-shape input).
use IterTools\Reduce;

$users = ['alice' => 12, 'bob' => 17, 'carol' => 22, 'dan' => 30];

$lastAdultName = Reduce::toLastMatchKey($users, fn (int $age) => $age >= 18);
// 'dan'

To Max

Reduces to the max value.

Reduce::toMax(iterable $data, callable $compareBy = null): mixed|null

  • Optional callable param $compareBy must return comparable value.
  • If $compareBy is not provided then items of given collection must be comparable.
  • Returns null if collection is empty.
use IterTools\Reduce;

$numbers = [5, 3, 1, 2, 4];

$result = Reduce::toMax($numbers);
// 5

$movieRatings = [
    [
        'title' => 'Star Wars: Episode IV - A New Hope',
        'rating' => 4.6
    ],
    [
        'title' => 'Star Wars: Episode V - The Empire Strikes Back',
        'rating' => 4.8
    ],
    [
        'title' => 'Star Wars: Episode VI - Return of the Jedi',
        'rating' => 4.6
    ],
];
$compareBy = fn ($movie) => $movie['rating'];

$highestRatedMovie = Reduce::toMax($movieRatings, $compareBy);
// [
//     'title' => 'Star Wars: Episode V - The Empire Strikes Back',
//     'rating' => 4.8
// ];

To Median

Reduces to the median value.

Reduce::toMedian(iterable $data): int|float|null

  • For an even number of elements, the median is the mean of the two middle values, computed so that it neither overflows when the two middle values sum beyond PHP_FLOAT_MAX nor loses precision when their span exceeds the integer range (toMedian([PHP_INT_MIN, PHP_INT_MAX]) is -0.5, not 0.0). Two identical middle values return that value, including INF.
  • Returns null if collection is empty.
use IterTools\Reduce;

$grades = [100, 90, 95, 85, 94];

$median = Reduce::toMedian($grades);
// 94

To Min

Reduces to the min value.

Reduce::toMin(iterable $data, callable $compareBy = null): mixed|null

  • Optional callable param $compareBy must return comparable value.
  • If $compareBy is not provided then items of given collection must be comparable.
  • Returns null if collection is empty.
use IterTools\Reduce;

$numbers = [5, 3, 1, 2, 4];

$result = Reduce::toMin($numbers);
// 1


$movieRatings = [
    [
        'title' => 'The Matrix',
        'rating' => 4.7
    ],
    [
        'title' => 'The Matrix Reloaded',
        'rating' => 4.3
    ],
    [
        'title' => 'The Matrix Revolutions',
        'rating' => 3.9
    ],
    [
        'title' => 'The Matrix Resurrections',
        'rating' => 2.5
    ],
];
$compareBy = fn ($movie) => $movie['rating'];

$lowestRatedMovie = Reduce::toMin($movieRatings, $compareBy);
// [
//     'title' => 'The Matrix Resurrections',
//     'rating' => 2.5
// ]

To Min Max

Reduces to array of its upper and lower bounds (max and min).

Reduce::toMinMax(iterable $numbers, callable $compareBy = null): array

  • Optional callable param $compareBy must return comparable value.
  • If $compareBy is not provided then items of given collection must be comparable.
  • Returns [null, null] if given collection is empty.
use IterTools\Reduce;

$numbers = [1, 2, 7, -1, -2, -3];

[$min, $max] = Reduce::toMinMax($numbers);
// [-3, 7]

$reportCard = [
    [
        'subject' => 'history',
        'grade' => 90
    ],
    [
        'subject' => 'math',
        'grade' => 98
    ],
    [
        'subject' => 'science',
        'grade' => 92
    ],
    [
        'subject' => 'english',
        'grade' => 85
    ],
    [
        'subject' => 'programming',
        'grade' => 100
    ],
];
$compareBy = fn ($class) => $class['grade'];

$bestAndWorstSubject = Reduce::toMinMax($reportCard, $compareBy);
// [
//     [
//         'subject' => 'english',
//         'grade' => 85
//     ],
//     [
//         'subject' => 'programming',
//         'grade' => 100
//     ],
// ]

To Mode

Reduces to a list of its modes (the most frequent values).

Reduce::toMode(iterable $data): array

  • Returns every value tied for the maximum frequency, in first-seen order (an all-unique input returns all of its values).
  • Multimodal inputs return multiple modes.
  • Values are compared strictly, so 1, 1.0, and '1' count as distinct.
  • Returns an empty array if collection is empty.
use IterTools\Reduce;

$votes = ['red', 'blue', 'red', 'green', 'blue', 'red'];

$modes = Reduce::toMode($votes);
// ['red']

To Nth

Reduces to value at the nth position.

Reduce::toNth(iterable $data, int $position): mixed

use IterTools\Reduce;

$lotrMovies = ['The Fellowship of the Ring', 'The Two Towers', 'The Return of the King'];

$rotk = Reduce::toNth($lotrMovies, 2);
// 20

To Only

Reduces iterable to its sole element.

Reduce::toOnly(iterable $data): mixed

  • Throws \LengthException if the iterable is empty or contains more than one element.
  • For associative single-element input, returns the value (not the key).
  • Compose with Stream::filter()->toOnly() to assert that exactly one item matches a predicate.
use IterTools\Reduce;

$config = ['admin' => 'jane'];

$onlyAdmin = Reduce::toOnly($config);
// 'jane'
use IterTools\Reduce;

Reduce::toOnly([]);        // throws \LengthException
Reduce::toOnly([1, 2, 3]); // throws \LengthException

To Percentile

Reduces to its value at the given percentile.

Reduce::toPercentile(iterable $data, float $percentile): int|float|null

  • Uses the R-7 / linear-interpolation method (the NumPy default). Percentile 0 is the minimum, 100 is the maximum.
  • $percentile must be in the inclusive range [0, 100]; otherwise throws \InvalidArgumentException.
  • The interpolation does not overflow when the two neighbouring values span more than PHP_FLOAT_MAX.
  • Percentile 50 returns exactly what toMedian returns, for every input.
  • Returns null if collection is empty.
use IterTools\Reduce;

$scores = [10, 20, 30, 40, 50];

$p75 = Reduce::toPercentile($scores, 75);
// 40

To Product

Reduces to the product of its elements.

Returns null if collection is empty.

Reduce::toProduct(iterable $data): number|null

use IterTools\Reduce;

$primeFactors = [5, 2, 2];

$number = Reduce::toProduct($primeFactors);
// 20

To Quantile

Reduces to its value at the given quantile.

Reduce::toQuantile(iterable $data, float $quantile): int|float|null

  • Thin wrapper over toPercentile that accepts a quantile in the inclusive range [0, 1] (e.g. 0.25 is the first quartile / 25th percentile).
  • $quantile must be in the inclusive range [0, 1]; otherwise throws \InvalidArgumentException.
  • Returns null if collection is empty.
use IterTools\Reduce;

$scores = [10, 20, 30, 40, 50];

$q3 = Reduce::toQuantile($scores, 0.75);
// 40

To Random Value

Reduces given collection to a random value within it.

Reduce::toRandomValue(iterable $data): mixed

use IterTools\Reduce;

$sfWakeupOptions = ['mid', 'low', 'overhead', 'throw', 'meaty'];

$wakeupOption = Reduce::toRandomValue($sfWakeupOptions);
// e.g., throw

To Range

Reduces given collection to its range (difference between max and min).

Reduce::toRange(iterable $numbers): int|float

Returns 0 if iterable source is empty.

use IterTools\Reduce;

$grades = [100, 90, 80, 85, 95];

$range = Reduce::toRange($numbers);
// 20

To Standard Deviation

Reduces to the standard deviation of its values.

Reduce::toStandardDeviation(iterable $data, bool $sample = false): float|null

  • Square root of the variance. Population standard deviation by default; pass $sample = true for the sample standard deviation (Bessel's correction).
  • Inherits the single-pass, O(1)-memory and overflow behavior of toVariance.
  • Returns null if collection is empty, or for the sample standard deviation of a single value. The population standard deviation of a single value is 0.0.
  • The null cases take precedence over NAN, matching toVariance: toStandardDeviation([INF], true) is null, while toStandardDeviation([INF]) is NAN.
use IterTools\Reduce;

$numbers = [2, 4, 4, 4, 5, 5, 7, 9];

$populationStdDev = Reduce::toStandardDeviation($numbers);
// 2.0

$sampleStdDev = Reduce::toStandardDeviation($numbers, true);
// 2.138...

To String

Reduces to a string joining all elements.

  • Optional separator to insert between items.
  • Optional prefix to prepend to the string.
  • Optional suffix to append to the string.

Reduce::toString(iterable $data, string $separator = '', string $prefix = '', string $suffix = ''): string

use IterTools\Reduce;

$words = ['IterTools', 'PHP', 'v1.0'];

$string = Reduce::toString($words);
// IterToolsPHPv1.0
$string = Reduce::toString($words, '-');
// IterTools-PHP-v1.0
$string = Reduce::toString($words, '-', 'Library: ');
// Library: IterTools-PHP-v1.0
$string = Reduce::toString($words, '-', 'Library: ', '!');
// Library: IterTools-PHP-v1.0!

To Sum

Reduces to the sum of its elements.

Reduce::toSum(iterable $data): number

use IterTools\Reduce;

$parts = [10, 20, 30];

$sum = Reduce::toSum($parts);
// 60

To Value

Reduce elements to a single value using reducer function.

Reduce::toValue(iterable $data, callable $reducer, mixed $initialValue): mixed

use IterTools\Reduce;

$input = [1, 2, 3, 4, 5];
$sum   = fn ($carry, $item) => $carry + $item;

$result = Reduce::toValue($input, $sum, 0);
// 15

To Variance

Reduces to the variance of its values.

Reduce::toVariance(iterable $data, bool $sample = false): float|null

  • Population variance by default; pass $sample = true for the sample variance (Bessel's correction — divides by N - 1).
  • Uses a scaled online algorithm with a compensated running mean: a single pass in O(1) memory, so the collection is never materialized and large lazy iterables are safe.
  • The result is stable across orderings of the input to within floating-point rounding. It is not bit-reproducible — floating-point addition is not associative, so different orderings may differ in the last ulp. The compensated mean does remove the gross order-dependence that an uncompensated one has for a large offset with a small spread (e.g. 1e16, 1e16 + 2, 1e16 + 4, where the answer would otherwise vary by 50% with ordering).
  • Stays finite whenever the variance is representable, even when intermediate quantities are not: the variance of [-1.4e154, 1.4e154, 0] is ~1.31e308, though the variance of its leading pair (1.96e308) already exceeds PHP_FLOAT_MAX.
  • A non-finite value anywhere in the input yields NAN, since deviations from an infinite mean are INF - INF. A variance that is itself too large to represent is reported as INF, never as a negative number.
  • Returns null if collection is empty, or for the sample variance of a single value (N - 1 = 0 is undefined). The population variance of a single value is 0.0.
  • The null cases take precedence over NAN: toVariance([INF], true) is null, not NAN, because with a single observation there is no sample variance to compute at all, whatever that observation happens to be. The population variance of one value is defined, so toVariance([INF]) is NAN.
use IterTools\Reduce;

$numbers = [1, 2, 3, 4, 5];

$populationVariance = Reduce::toVariance($numbers);
// 2.0

$sampleVariance = Reduce::toVariance($numbers, true);
// 2.5

Consume

Drains the given iterable, discarding values.

Reduce::consume(iterable $data): void

  • Useful for forcing evaluation of a lazy pipeline whose only purpose is its side effects.
  • Returns nothing.
use IterTools\Reduce;
use IterTools\Single;

$log = [];

$pipeline = Single::map([1, 2, 3], function (int $n) use (&$log): int {
    $log[] = $n;
    return $n * 2;
});
// $log === []  (Single::map is lazy — nothing has run yet)

Reduce::consume($pipeline);
// $log === [1, 2, 3]