From 502dba20f281245ad39554c0ffd162f87f06fb72 Mon Sep 17 00:00:00 2001 From: PresentKim Date: Wed, 7 Oct 2026 03:16:56 +0000 Subject: [PATCH 1/2] perf: Avoid array copies and pass only declared callback arguments - Build results by cloning an empty instance and calling exchangeArray(), so the userland constructor no longer makes ArrayObject copy the array - Pass only the arguments a callback declares, delegating to array_map(), array_filter() and array_reduce() when possible - Use array_key_last() on PHP 7.3+ for last()/keyLast() - Add @Warmup to benchmarks that lacked it, so the one-time autoload cost is not counted - Add unit tests for the callback arity paths --- src/kim/present/lib/arrayutils/ArrayUtils.php | 44 ++++++ .../lib/arrayutils/traits/ConcatTrait.php | 45 +++++- .../lib/arrayutils/traits/IterationTrait.php | 145 +++++++++++++++++- .../lib/arrayutils/traits/SearchTrait.php | 44 +++++- .../lib/arrayutils/traits/SetTrait.php | 34 ++-- .../lib/arrayutils/traits/ShapeTrait.php | 42 +++-- .../lib/arrayutils/traits/SortTrait.php | 14 +- .../lib/arrayutils/traits/StackTrait.php | 10 +- tests/benchmarks/FirstLastKeyExistsBench.php | 4 + tests/benchmarks/UniqueReplaceBench.php | 2 + tests/unit/ArrayUtilsTest.php | 54 +++++++ 11 files changed, 396 insertions(+), 42 deletions(-) diff --git a/src/kim/present/lib/arrayutils/ArrayUtils.php b/src/kim/present/lib/arrayutils/ArrayUtils.php index 51614f4..c6d3c3d 100644 --- a/src/kim/present/lib/arrayutils/ArrayUtils.php +++ b/src/kim/present/lib/arrayutils/ArrayUtils.php @@ -28,6 +28,10 @@ namespace kim\present\lib\arrayutils; use ArrayObject; +use Closure; +use ReflectionException; +use ReflectionFunction; +use ReflectionMethod; use Traversable; use kim\present\lib\arrayutils\traits\ConcatTrait; use kim\present\lib\arrayutils\traits\IterationTrait; @@ -38,7 +42,9 @@ use kim\present\lib\arrayutils\traits\StackTrait; use function is_array; +use function is_string; use function iterator_to_array; +use function strpos; /** * Class ArrayUtils is provides a method to fancy manipulate an array @@ -64,6 +70,9 @@ class ArrayUtils extends ArrayObject{ use SortTrait; use StackTrait; + /** @var ArrayUtils|null Empty instance that is cloned to create results without calling the constructor */ + private static $blank = null; + /** Creates a new, shallow-copied ArrayUtils instance from an iterable */ public function __construct(iterable $iterable, int $flags = 0, string $iteratorClass = "ArrayIterator"){ parent::__construct(is_array($iterable) ? $iterable : self::toArray($iterable), $flags, $iteratorClass); @@ -123,6 +132,41 @@ public static function toArray(iterable $iterable) : array{ return iterator_to_array($iterable, true); } + /** + * Creates an empty instance without running the constructor + * + * The userland constructor forwards its argument to ArrayObject, which makes ArrayObject copy the whole array. + * Filling a clone with exchangeArray() takes the array over without copying it. + */ + private static function blank() : ArrayUtils{ + if(self::$blank === null){ + self::$blank = new self([]); + } + return clone self::$blank; + } + + /** + * Returns how many arguments the callback accepts (3 if it cannot be determined or is variadic) + * Iteration methods use this to pass only the arguments the callback declares, + * which lets them delegate to native functions such as array_map() and array_filter() + */ + private static function callbackArity(callable $callback) : int{ + try{ + if($callback instanceof Closure || (is_string($callback) && strpos($callback, "::") === false)){ + $reflection = new ReflectionFunction($callback); + }elseif(is_array($callback)){ + $reflection = new ReflectionMethod($callback[0], $callback[1]); + }elseif(is_string($callback)){ + $reflection = new ReflectionMethod($callback); + }else{ + $reflection = new ReflectionMethod($callback, "__invoke"); + } + return $reflection->isVariadic() ? 3 : $reflection->getNumberOfParameters(); + }catch(ReflectionException $_){ + return 3; + } + } + /** Exchange the array for another one */ public function exchange(array $array) : ArrayUtils{ $this->exchangeArray($array); diff --git a/src/kim/present/lib/arrayutils/traits/ConcatTrait.php b/src/kim/present/lib/arrayutils/traits/ConcatTrait.php index 45f01f4..df9246a 100644 --- a/src/kim/present/lib/arrayutils/traits/ConcatTrait.php +++ b/src/kim/present/lib/arrayutils/traits/ConcatTrait.php @@ -35,7 +35,7 @@ /** * Methods that join arrays together (concat, merge, replace, flat) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait ConcatTrait{ /** @@ -59,7 +59,9 @@ public function concatAs(...$values) : array{ * Same as concat(), but operates on the given iterable and returns a new ArrayUtils */ public static function concatFrom(iterable $from, ...$values) : ArrayUtils{ - return new self(array_merge((is_array($from) ? $from : self::toArray($from)), ...self::mapToArray($values))); + $utils = self::blank(); + $utils->exchangeArray(array_merge((is_array($from) ? $from : self::toArray($from)), ...self::mapToArray($values))); + return $utils; } /** @@ -90,7 +92,9 @@ public function concatSoftAs(...$values) : array{ * Same as concatSoft(), but operates on the given iterable and returns a new ArrayUtils */ public static function concatSoftFrom(iterable $from, ...$values) : ArrayUtils{ - return new self(self::concatSoftFromAs($from, ...$values)); + $utils = self::blank(); + $utils->exchangeArray(self::concatSoftFromAs($from, ...$values)); + return $utils; } /** @@ -125,7 +129,9 @@ public function mergeAs(...$values) : array{ * Same as merge(), but operates on the given iterable and returns a new ArrayUtils */ public static function mergeFrom(iterable $from, ...$values) : ArrayUtils{ - return new self(array_merge((is_array($from) ? $from : self::toArray($from)), ...self::mapToArray($values))); + $utils = self::blank(); + $utils->exchangeArray(array_merge((is_array($from) ? $from : self::toArray($from)), ...self::mapToArray($values))); + return $utils; } /** @@ -156,7 +162,9 @@ public function mergeSoftAs(...$values) : array{ * Same as mergeSoft(), but operates on the given iterable and returns a new ArrayUtils */ public static function mergeSoftFrom(iterable $from, ...$values) : ArrayUtils{ - return new self(self::mergeSoftFromAs($from, ...$values)); + $utils = self::blank(); + $utils->exchangeArray(self::mergeSoftFromAs($from, ...$values)); + return $utils; } /** @@ -188,7 +196,9 @@ public function replaceAs(iterable ...$iterables) : array{ * Same as replace(), but operates on the given iterable and returns a new ArrayUtils */ public static function replaceFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ - return new self(array_replace_recursive((is_array($from) ? $from : self::toArray($from)), ...self::mapToArray($iterables))); + $utils = self::blank(); + $utils->exchangeArray(array_replace_recursive((is_array($from) ? $from : self::toArray($from)), ...self::mapToArray($iterables))); + return $utils; } /** @@ -219,7 +229,9 @@ public function flatAs(int $dept = 1) : array{ * Same as flat(), but operates on the given iterable and returns a new ArrayUtils */ public static function flatFrom(iterable $from, int $dept = 1) : ArrayUtils{ - return new self(self::flatFromAs($from, $dept)); + $utils = self::blank(); + $utils->exchangeArray(self::flatFromAs($from, $dept)); + return $utils; } /** @@ -258,7 +270,9 @@ public function flatMapAs(callable $callback) : array{ * Same as flatMap(), but operates on the given iterable and returns a new ArrayUtils */ public static function flatMapFrom(iterable $from, callable $callback) : ArrayUtils{ - return new self(self::flatMapFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::flatMapFromAs($from, $callback)); + return $utils; } /** @@ -266,6 +280,21 @@ public static function flatMapFrom(iterable $from, callable $callback) : ArrayUt */ public static function flatMapFromAs(iterable $from, callable $callback) : array{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + $parts = []; + foreach($array as $key => $value){ + $parts[] = (array) $callback($value); + } + return $parts ? array_merge(...$parts) : []; + } + if($arity <= 2){ + $parts = []; + foreach($array as $key => $value){ + $parts[] = (array) $callback($value, $key); + } + return $parts ? array_merge(...$parts) : []; + } $parts = []; foreach($array as $key => $value){ $parts[] = (array) $callback($value, $key, $array); diff --git a/src/kim/present/lib/arrayutils/traits/IterationTrait.php b/src/kim/present/lib/arrayutils/traits/IterationTrait.php index 4182d41..9ec3f81 100644 --- a/src/kim/present/lib/arrayutils/traits/IterationTrait.php +++ b/src/kim/present/lib/arrayutils/traits/IterationTrait.php @@ -28,6 +28,10 @@ namespace kim\present\lib\arrayutils\traits; use kim\present\lib\arrayutils\ArrayUtils; +use const ARRAY_FILTER_USE_BOTH; +use function array_filter; +use function array_map; +use function array_reduce; use function array_reverse; use function array_sum; use function is_array; @@ -35,7 +39,7 @@ /** * Methods that walk every element with a callback (map, filter, forEach, reduce, every, some) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait IterationTrait{ /** @@ -59,7 +63,9 @@ public function mapAs(callable $callback) : array{ * Same as map(), but operates on the given iterable and returns a new ArrayUtils */ public static function mapFrom(iterable $from, callable $callback) : ArrayUtils{ - return new self(self::mapFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::mapFromAs($from, $callback)); + return $utils; } /** @@ -67,6 +73,17 @@ public static function mapFrom(iterable $from, callable $callback) : ArrayUtils{ */ public static function mapFromAs(iterable $from, callable $callback) : array{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + return array_map($callback, $array); + } + if($arity <= 2){ + $result = []; + foreach($array as $key => $value){ + $result[$key] = $callback($value, $key); + } + return $result; + } $result = []; foreach($array as $key => $value){ $result[$key] = $callback($value, $key, $array); @@ -95,7 +112,9 @@ public function mapAssocAs(callable $callback) : array{ * Same as mapAssoc(), but operates on the given iterable and returns a new ArrayUtils */ public static function mapAssocFrom(iterable $from, callable $callback) : ArrayUtils{ - return new self(self::mapAssocFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::mapAssocFromAs($from, $callback)); + return $utils; } /** @@ -103,6 +122,23 @@ public static function mapAssocFrom(iterable $from, callable $callback) : ArrayU */ public static function mapAssocFromAs(iterable $from, callable $callback) : array{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + $result = []; + foreach($array as $key => $value){ + [$newKey, $newValue] = $callback($value); + $result[$newKey] = $newValue; + } + return $result; + } + if($arity <= 2){ + $result = []; + foreach($array as $key => $value){ + [$newKey, $newValue] = $callback($value, $key); + $result[$newKey] = $newValue; + } + return $result; + } $result = []; foreach($array as $key => $value){ [$newKey, $newValue] = $callback($value, $key, $array); @@ -132,7 +168,9 @@ public function mapKeyAs(callable $callback) : array{ * Same as mapKey(), but operates on the given iterable and returns a new ArrayUtils */ public static function mapKeyFrom(iterable $from, callable $callback) : ArrayUtils{ - return new self(self::mapKeyFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::mapKeyFromAs($from, $callback)); + return $utils; } /** @@ -140,6 +178,21 @@ public static function mapKeyFrom(iterable $from, callable $callback) : ArrayUti */ public static function mapKeyFromAs(iterable $from, callable $callback) : array{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + $result = []; + foreach($array as $key => $value){ + $result[$callback($value)] = $value; + } + return $result; + } + if($arity <= 2){ + $result = []; + foreach($array as $key => $value){ + $result[$callback($value, $key)] = $value; + } + return $result; + } $result = []; foreach($array as $key => $value){ $result[$callback($value, $key, $array)] = $value; @@ -168,7 +221,9 @@ public function filterAs(callable $callback) : array{ * Same as filter(), but operates on the given iterable and returns a new ArrayUtils */ public static function filterFrom(iterable $from, callable $callback) : ArrayUtils{ - return new self(self::filterFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::filterFromAs($from, $callback)); + return $utils; } /** @@ -176,6 +231,13 @@ public static function filterFrom(iterable $from, callable $callback) : ArrayUti */ public static function filterFromAs(iterable $from, callable $callback) : array{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + return array_filter($array, $callback); + } + if($arity <= 2){ + return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH); + } $result = []; foreach($array as $key => $value){ if($callback($value, $key, $array)){ @@ -206,7 +268,9 @@ public function forEachAs(callable $callback) : array{ * Same as forEach(), but operates on the given iterable and returns a new ArrayUtils */ public static function forEachFrom(iterable $from, callable $callback) : ArrayUtils{ - return new self(self::forEachFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::forEachFromAs($from, $callback)); + return $utils; } /** @@ -214,6 +278,19 @@ public static function forEachFrom(iterable $from, callable $callback) : ArrayUt */ public static function forEachFromAs(iterable $from, callable $callback) : array{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + foreach($array as $key => $value){ + $callback($value); + } + return $array; + } + if($arity <= 2){ + foreach($array as $key => $value){ + $callback($value, $key); + } + return $array; + } foreach($array as $key => $value){ $callback($value, $key, $array); } @@ -234,6 +311,23 @@ public function every(callable $callback) : bool{ */ public static function everyFrom(iterable $from, callable $callback) : bool{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + foreach($array as $key => $value){ + if(!$callback($value)){ + return false; + } + } + return true; + } + if($arity <= 2){ + foreach($array as $key => $value){ + if(!$callback($value, $key)){ + return false; + } + } + return true; + } foreach($array as $key => $value){ if(!$callback($value, $key, $array)){ return false; @@ -256,6 +350,23 @@ public function some(callable $callback) : bool{ */ public static function someFrom(iterable $from, callable $callback) : bool{ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + foreach($array as $key => $value){ + if($callback($value)){ + return true; + } + } + return false; + } + if($arity <= 2){ + foreach($array as $key => $value){ + if($callback($value, $key)){ + return true; + } + } + return false; + } foreach($array as $key => $value){ if($callback($value, $key, $array)){ return true; @@ -278,6 +389,17 @@ public function reduce(callable $callback, $initialValue = null){ */ public static function reduceFrom(iterable $from, callable $callback, $initialValue = null){ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 2){ + return array_reduce($array, $callback, $initialValue); + } + if($arity <= 3){ + $currentValue = $initialValue; + foreach($array as $key => $value){ + $currentValue = $callback($currentValue, $value, $key); + } + return $currentValue; + } $currentValue = $initialValue; foreach($array as $key => $value){ $currentValue = $callback($currentValue, $value, $key, $array); @@ -299,6 +421,17 @@ public function reduceRight(callable $callback, $initialValue = null){ */ public static function reduceRightFrom(iterable $from, callable $callback, $initialValue = null){ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 2){ + return array_reduce(array_reverse($array), $callback, $initialValue); + } + if($arity <= 3){ + $currentValue = $initialValue; + foreach(array_reverse($array, true) as $key => $value){ + $currentValue = $callback($currentValue, $value, $key); + } + return $currentValue; + } $currentValue = $initialValue; foreach(array_reverse($array, true) as $key => $value){ $currentValue = $callback($currentValue, $value, $key, $array); diff --git a/src/kim/present/lib/arrayutils/traits/SearchTrait.php b/src/kim/present/lib/arrayutils/traits/SearchTrait.php index 3b9c1f2..53ac111 100644 --- a/src/kim/present/lib/arrayutils/traits/SearchTrait.php +++ b/src/kim/present/lib/arrayutils/traits/SearchTrait.php @@ -30,6 +30,7 @@ use kim\present\lib\arrayutils\ArrayUtils; use Exception; use function array_key_exists; +use function array_key_last; use function array_keys; use function array_search; use function array_slice; @@ -46,7 +47,7 @@ /** * Methods that look up a single element or key (includes, indexOf, find, first, last, random) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait SearchTrait{ /** @@ -148,6 +149,23 @@ public function find(callable $callback){ */ public static function findFrom(iterable $from, callable $callback){ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + foreach($array as $key => $value){ + if($callback($value)){ + return $value; + } + } + return null; + } + if($arity <= 2){ + foreach($array as $key => $value){ + if($callback($value, $key)){ + return $value; + } + } + return null; + } foreach($array as $key => $value){ if($callback($value, $key, $array)){ return $value; @@ -174,6 +192,23 @@ public function findIndex(callable $callback){ */ public static function findIndexFrom(iterable $from, callable $callback){ $array = is_array($from) ? $from : self::toArray($from); + $arity = self::callbackArity($callback); + if($arity <= 1){ + foreach($array as $key => $value){ + if($callback($value)){ + return $key; + } + } + return null; + } + if($arity <= 2){ + foreach($array as $key => $value){ + if($callback($value, $key)){ + return $key; + } + } + return null; + } foreach($array as $key => $value){ if($callback($value, $key, $array)){ return $key; @@ -240,6 +275,10 @@ public function last(){ */ public static function lastFrom(iterable $from){ $array = is_array($from) ? $from : self::toArray($from); + if(\PHP_VERSION_ID >= 70300){ + $key = array_key_last($array); + return $key === null ? null : $array[$key]; + } return $array ? end($array) : null; } @@ -261,6 +300,9 @@ public function keyLast(){ */ public static function keyLastFrom(iterable $from){ $array = is_array($from) ? $from : self::toArray($from); + if(\PHP_VERSION_ID >= 70300){ + return array_key_last($array); + } if(!$array){ return null; } diff --git a/src/kim/present/lib/arrayutils/traits/SetTrait.php b/src/kim/present/lib/arrayutils/traits/SetTrait.php index ac79e28..87a046a 100644 --- a/src/kim/present/lib/arrayutils/traits/SetTrait.php +++ b/src/kim/present/lib/arrayutils/traits/SetTrait.php @@ -41,7 +41,7 @@ /** * Methods that compare arrays or collapse duplicates (diff, intersect, unique, countValues) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait SetTrait{ /** @@ -68,7 +68,9 @@ public function diffAs(iterable ...$iterables) : array{ */ public static function diffFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self($iterables ? array_diff($a, ...self::mapToArray($iterables)) : $a); + $utils = self::blank(); + $utils->exchangeArray($iterables ? array_diff($a, ...self::mapToArray($iterables)) : $a); + return $utils; } /** @@ -103,7 +105,9 @@ public function diffAssocAs(iterable ...$iterables) : array{ */ public static function diffAssocFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self($iterables ? array_diff_assoc($a, ...self::mapToArray($iterables)) : $a); + $utils = self::blank(); + $utils->exchangeArray($iterables ? array_diff_assoc($a, ...self::mapToArray($iterables)) : $a); + return $utils; } /** @@ -138,7 +142,9 @@ public function diffKeyAs(iterable ...$iterables) : array{ */ public static function diffKeyFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self($iterables ? array_diff_key($a, ...self::mapToArray($iterables)) : $a); + $utils = self::blank(); + $utils->exchangeArray($iterables ? array_diff_key($a, ...self::mapToArray($iterables)) : $a); + return $utils; } /** @@ -173,7 +179,9 @@ public function intersectAs(iterable ...$iterables) : array{ */ public static function intersectFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self($iterables ? array_intersect($a, ...self::mapToArray($iterables)) : $a); + $utils = self::blank(); + $utils->exchangeArray($iterables ? array_intersect($a, ...self::mapToArray($iterables)) : $a); + return $utils; } /** @@ -208,7 +216,9 @@ public function intersectAssocAs(iterable ...$iterables) : array{ */ public static function intersectAssocFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self($iterables ? array_intersect_assoc($a, ...self::mapToArray($iterables)) : $a); + $utils = self::blank(); + $utils->exchangeArray($iterables ? array_intersect_assoc($a, ...self::mapToArray($iterables)) : $a); + return $utils; } /** @@ -243,7 +253,9 @@ public function intersectKeyAs(iterable ...$iterables) : array{ */ public static function intersectKeyFrom(iterable $from, iterable ...$iterables) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self($iterables ? array_intersect_key($a, ...self::mapToArray($iterables)) : $a); + $utils = self::blank(); + $utils->exchangeArray($iterables ? array_intersect_key($a, ...self::mapToArray($iterables)) : $a); + return $utils; } /** @@ -275,7 +287,9 @@ public function uniqueAs(int $sortFlags = SORT_STRING) : array{ * Same as unique(), but operates on the given iterable and returns a new ArrayUtils */ public static function uniqueFrom(iterable $from, int $sortFlags = SORT_STRING) : ArrayUtils{ - return new self(array_unique((is_array($from) ? $from : self::toArray($from)), $sortFlags)); + $utils = self::blank(); + $utils->exchangeArray(array_unique((is_array($from) ? $from : self::toArray($from)), $sortFlags)); + return $utils; } /** @@ -306,7 +320,9 @@ public function countValuesAs() : array{ * Same as countValues(), but operates on the given iterable and returns a new ArrayUtils */ public static function countValuesFrom(iterable $from) : ArrayUtils{ - return new self(array_count_values((is_array($from) ? $from : self::toArray($from)))); + $utils = self::blank(); + $utils->exchangeArray(array_count_values((is_array($from) ? $from : self::toArray($from)))); + return $utils; } /** diff --git a/src/kim/present/lib/arrayutils/traits/ShapeTrait.php b/src/kim/present/lib/arrayutils/traits/ShapeTrait.php index 748c264..b243c97 100644 --- a/src/kim/present/lib/arrayutils/traits/ShapeTrait.php +++ b/src/kim/present/lib/arrayutils/traits/ShapeTrait.php @@ -46,7 +46,7 @@ /** * Methods that change the shape of the array (chunk, column, combine, fill, flip, keys, values, pad, slice, join) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait ShapeTrait{ /** @@ -70,7 +70,9 @@ public function chunkAs(int $size, bool $preserveKeys = false) : array{ * Same as chunk(), but operates on the given iterable and returns a new ArrayUtils */ public static function chunkFrom(iterable $from, int $size, bool $preserveKeys = false) : ArrayUtils{ - return new self(array_chunk((is_array($from) ? $from : self::toArray($from)), $size, $preserveKeys)); + $utils = self::blank(); + $utils->exchangeArray(array_chunk((is_array($from) ? $from : self::toArray($from)), $size, $preserveKeys)); + return $utils; } /** @@ -101,7 +103,9 @@ public function columnAs($valueKey, $indexKey = null) : array{ * Same as column(), but operates on the given iterable and returns a new ArrayUtils */ public static function columnFrom(iterable $from, $valueKey, $indexKey = null) : ArrayUtils{ - return new self(array_column((is_array($from) ? $from : self::toArray($from)), $valueKey, $indexKey)); + $utils = self::blank(); + $utils->exchangeArray(array_column((is_array($from) ? $from : self::toArray($from)), $valueKey, $indexKey)); + return $utils; } /** @@ -136,7 +140,9 @@ public function combineAs(?iterable $valueArray = null) : array{ */ public static function combineFrom(iterable $from, ?iterable $valueArray = null) : ArrayUtils{ $a = (is_array($from) ? $from : self::toArray($from)); - return new self(array_combine($a, (array) ($valueArray ?? $a))); + $utils = self::blank(); + $utils->exchangeArray(array_combine($a, (array) ($valueArray ?? $a))); + return $utils; } /** @@ -168,7 +174,9 @@ public function fillAs($value, int $start = 0, ?int $end = null) : array{ * Same as fill(), but operates on the given iterable and returns a new ArrayUtils */ public static function fillFrom(iterable $from, $value, int $start = 0, ?int $end = null) : ArrayUtils{ - return new self(self::fillFromAs($from, $value, $start, $end)); + $utils = self::blank(); + $utils->exchangeArray(self::fillFromAs($from, $value, $start, $end)); + return $utils; } /** @@ -205,7 +213,9 @@ public function fillKeysAs($value) : array{ * Same as fillKeys(), but operates on the given iterable and returns a new ArrayUtils */ public static function fillKeysFrom(iterable $from, $value) : ArrayUtils{ - return new self(array_fill_keys((is_array($from) ? $from : self::toArray($from)), $value)); + $utils = self::blank(); + $utils->exchangeArray(array_fill_keys((is_array($from) ? $from : self::toArray($from)), $value)); + return $utils; } /** @@ -236,7 +246,9 @@ public function flipAs() : array{ * Same as flip(), but operates on the given iterable and returns a new ArrayUtils */ public static function flipFrom(iterable $from) : ArrayUtils{ - return new self(array_flip((is_array($from) ? $from : self::toArray($from)))); + $utils = self::blank(); + $utils->exchangeArray(array_flip((is_array($from) ? $from : self::toArray($from)))); + return $utils; } /** @@ -267,7 +279,9 @@ public function keysAs() : array{ * Same as keys(), but operates on the given iterable and returns a new ArrayUtils */ public static function keysFrom(iterable $from) : ArrayUtils{ - return new self(array_keys((is_array($from) ? $from : self::toArray($from)))); + $utils = self::blank(); + $utils->exchangeArray(array_keys((is_array($from) ? $from : self::toArray($from)))); + return $utils; } /** @@ -298,7 +312,9 @@ public function valuesAs() : array{ * Same as values(), but operates on the given iterable and returns a new ArrayUtils */ public static function valuesFrom(iterable $from) : ArrayUtils{ - return new self(array_values((is_array($from) ? $from : self::toArray($from)))); + $utils = self::blank(); + $utils->exchangeArray(array_values((is_array($from) ? $from : self::toArray($from)))); + return $utils; } /** @@ -329,7 +345,9 @@ public function padAs(int $size, $value) : array{ * Same as pad(), but operates on the given iterable and returns a new ArrayUtils */ public static function padFrom(iterable $from, int $size, $value) : ArrayUtils{ - return new self(array_pad((is_array($from) ? $from : self::toArray($from)), $size, $value)); + $utils = self::blank(); + $utils->exchangeArray(array_pad((is_array($from) ? $from : self::toArray($from)), $size, $value)); + return $utils; } /** @@ -361,7 +379,9 @@ public function sliceAs(int $start = 0, ?int $end = null, bool $preserveKeys = f * Same as slice(), but operates on the given iterable and returns a new ArrayUtils */ public static function sliceFrom(iterable $from, int $start = 0, ?int $end = null, bool $preserveKeys = false) : ArrayUtils{ - return new self(self::sliceFromAs($from, $start, $end, $preserveKeys)); + $utils = self::blank(); + $utils->exchangeArray(self::sliceFromAs($from, $start, $end, $preserveKeys)); + return $utils; } /** diff --git a/src/kim/present/lib/arrayutils/traits/SortTrait.php b/src/kim/present/lib/arrayutils/traits/SortTrait.php index 213ed76..2b19089 100644 --- a/src/kim/present/lib/arrayutils/traits/SortTrait.php +++ b/src/kim/present/lib/arrayutils/traits/SortTrait.php @@ -38,7 +38,7 @@ /** * Methods that reorder the elements (sort, sortKey, reverse) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait SortTrait{ /** @@ -63,7 +63,9 @@ public function sortAs(?callable $callback = null) : array{ * Same as sort(), but operates on the given iterable and returns a new ArrayUtils */ public static function sortFrom(iterable $from, ?callable $callback = null) : ArrayUtils{ - return new self(self::sortFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::sortFromAs($from, $callback)); + return $utils; } /** @@ -101,7 +103,9 @@ public function sortKeyAs(?callable $callback = null) : array{ * Same as sortKey(), but operates on the given iterable and returns a new ArrayUtils */ public static function sortKeyFrom(iterable $from, ?callable $callback = null) : ArrayUtils{ - return new self(self::sortKeyFromAs($from, $callback)); + $utils = self::blank(); + $utils->exchangeArray(self::sortKeyFromAs($from, $callback)); + return $utils; } /** @@ -138,7 +142,9 @@ public function reverseAs(bool $preserveKeys = false) : array{ * Same as reverse(), but operates on the given iterable and returns a new ArrayUtils */ public static function reverseFrom(iterable $from, bool $preserveKeys = false) : ArrayUtils{ - return new self(array_reverse((is_array($from) ? $from : self::toArray($from)), $preserveKeys)); + $utils = self::blank(); + $utils->exchangeArray(array_reverse((is_array($from) ? $from : self::toArray($from)), $preserveKeys)); + return $utils; } /** diff --git a/src/kim/present/lib/arrayutils/traits/StackTrait.php b/src/kim/present/lib/arrayutils/traits/StackTrait.php index a581a40..d204735 100644 --- a/src/kim/present/lib/arrayutils/traits/StackTrait.php +++ b/src/kim/present/lib/arrayutils/traits/StackTrait.php @@ -39,7 +39,7 @@ /** * Methods that add or remove elements at a position (push, pop, shift, unshift, splice) * - * Requires ArrayUtils::toArray() and ArrayUtils::mapToArray() of the class using this trait + * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait */ trait StackTrait{ /** @@ -63,7 +63,9 @@ public function pushAs(...$values) : array{ * Same as push(), but operates on the given iterable and returns a new ArrayUtils */ public static function pushFrom(iterable $from, ...$values) : ArrayUtils{ - return new self(self::pushFromAs($from, ...$values)); + $utils = self::blank(); + $utils->exchangeArray(self::pushFromAs($from, ...$values)); + return $utils; } /** @@ -98,7 +100,9 @@ public function unshiftAs(...$values) : array{ * Same as unshift(), but operates on the given iterable and returns a new ArrayUtils */ public static function unshiftFrom(iterable $from, ...$values) : ArrayUtils{ - return new self(self::unshiftFromAs($from, ...$values)); + $utils = self::blank(); + $utils->exchangeArray(self::unshiftFromAs($from, ...$values)); + return $utils; } /** diff --git a/tests/benchmarks/FirstLastKeyExistsBench.php b/tests/benchmarks/FirstLastKeyExistsBench.php index d03938e..307325a 100644 --- a/tests/benchmarks/FirstLastKeyExistsBench.php +++ b/tests/benchmarks/FirstLastKeyExistsBench.php @@ -23,6 +23,7 @@ public function bench_first($params){ /** * @Revs(2048) * @Iterations(5) + * @Warmup(2) * @ParamProviders("provideMethods") */ public function bench_keyFirst($params){ @@ -37,6 +38,7 @@ public function bench_keyFirst($params){ /** * @Revs(2048) * @Iterations(5) + * @Warmup(2) * @ParamProviders("provideMethods") */ public function bench_last($params){ @@ -51,6 +53,7 @@ public function bench_last($params){ /** * @Revs(2048) * @Iterations(5) + * @Warmup(2) * @ParamProviders("provideMethods") */ public function bench_keyLast($params){ @@ -65,6 +68,7 @@ public function bench_keyLast($params){ /** * @Revs(2048) * @Iterations(5) + * @Warmup(2) * @ParamProviders("provideMethods") */ public function bench_keyExists($params){ diff --git a/tests/benchmarks/UniqueReplaceBench.php b/tests/benchmarks/UniqueReplaceBench.php index c32405d..e40088c 100644 --- a/tests/benchmarks/UniqueReplaceBench.php +++ b/tests/benchmarks/UniqueReplaceBench.php @@ -16,6 +16,7 @@ protected function initData() : void{ /** * @Revs(256) * @Iterations(3) + * @Warmup(2) * @ParamProviders("provideMethods") */ public function bench_unique($params){ @@ -29,6 +30,7 @@ public function bench_unique($params){ /** * @Revs(256) * @Iterations(3) + * @Warmup(2) * @ParamProviders("provideMethods") */ public function bench_replace($params){ diff --git a/tests/unit/ArrayUtilsTest.php b/tests/unit/ArrayUtilsTest.php index f7a3160..cfaaaea 100644 --- a/tests/unit/ArrayUtilsTest.php +++ b/tests/unit/ArrayUtilsTest.php @@ -80,4 +80,58 @@ public function testEmptyVariadicsAreSafe() : void{ $this->assertSame([1, 2], ArrayUtils::concatFromAs([1], [2])); $this->assertSame([], ArrayUtils::flatMapFromAs([], static function($v){ return $v; })); } + + public function testCallbacksReceiveOnlyDeclaredArguments() : void{ + $array = ["a" => 1, "b" => 2, "c" => 3]; + $one = static function($v){ return $v * 2; }; + $two = static function($v, $k){ return $k . $v; }; + $three = static function($v, $k, $all){ return $k . $v . count($all); }; + $variadic = static function(...$args){ return count($args); }; + + $this->assertSame(["a" => 2, "b" => 4, "c" => 6], ArrayUtils::mapFromAs($array, $one)); + $this->assertSame(["a" => "a1", "b" => "b2", "c" => "c3"], ArrayUtils::mapFromAs($array, $two)); + $this->assertSame(["a" => "a13", "b" => "b23", "c" => "c33"], ArrayUtils::mapFromAs($array, $three)); + $this->assertSame(["a" => 3, "b" => 3, "c" => 3], ArrayUtils::mapFromAs($array, $variadic)); + + $this->assertSame(["b" => 2, "c" => 3], ArrayUtils::filterFromAs($array, static function($v){ return $v > 1; })); + $this->assertSame(["c" => 3], ArrayUtils::filterFromAs($array, static function($v, $k){ return $k === "c"; })); + $this->assertSame(["a" => 1], ArrayUtils::filterFromAs($array, static function($v, $k, $all){ return $v === count($all) - 2; })); + + $this->assertSame(6, ArrayUtils::reduceFrom($array, static function($c, $v){ return $c + $v; }, 0)); + $this->assertSame("a1b2c3", ArrayUtils::reduceFrom($array, static function($c, $v, $k){ return $c . $k . $v; }, "")); + $this->assertSame("c3b2a1", ArrayUtils::reduceRightFrom($array, static function($c, $v, $k){ return $c . $k . $v; }, "")); + $this->assertSame(6, ArrayUtils::reduceRightFrom($array, static function($c, $v){ return $c + $v; }, 0)); + $this->assertTrue(ArrayUtils::everyFrom($array, static function($v){ return $v > 0; })); + $this->assertTrue(ArrayUtils::someFrom($array, static function($v, $k){ return $k === "b"; })); + $this->assertSame(2, ArrayUtils::findFrom($array, static function($v, $k){ return $k === "b"; })); + $this->assertSame("c", ArrayUtils::findIndexFrom($array, static function($v){ return $v === 3; })); + } + + public function testNonClosureCallbacks() : void{ + $this->assertSame(["A", "B"], ArrayUtils::mapFromAs(["a", "b"], "strtoupper")); + $this->assertSame([2, 4], ArrayUtils::mapFromAs([1, 2], [$this, "double"])); + $this->assertSame([3, 6], ArrayUtils::mapFromAs([1, 2], self::class . "::triple")); + $this->assertSame([5, 6], ArrayUtils::mapFromAs([1, 2], new class{ public function __invoke($v){ return $v + 4; } })); + } + + public function testResultsAreIndependentInstances() : void{ + $source = [1, 2, 3]; + $first = ArrayUtils::mapFrom($source, static function($v){ return $v; }); + $second = ArrayUtils::mapFrom($source, static function($v){ return $v * 2; }); + $first->push(4); + $this->assertSame([1, 2, 3, 4], $first->getArrayCopy()); + $this->assertSame([2, 4, 6], $second->getArrayCopy()); + $this->assertSame([1, 2, 3], $source); + } + + public function testLastAndKeyLast() : void{ + $this->assertSame(3, ArrayUtils::lastFrom(["a" => 1, 5 => 3])); + $this->assertSame(5, ArrayUtils::keyLastFrom(["a" => 1, 5 => 3])); + $this->assertFalse(ArrayUtils::lastFrom([1, false])); + $this->assertNull(ArrayUtils::keyLastFrom([])); + } + + public function double($v){ return $v * 2; } + + public static function triple($v){ return $v * 3; } } From f2de93a5e285eb2ee2614c02bad98ec9bc48c0b7 Mon Sep 17 00:00:00 2001 From: PresentKim Date: Wed, 7 Oct 2026 06:19:30 +0000 Subject: [PATCH 2/2] build: Add trait generator script and pop/shift tests - Add tools/generate-traits.py, the single source of the method traits (supports --check to verify the committed traits are up to date) - Mark generated traits with @generated - Test that pop() and shift() remove the element from the instance --- .../lib/arrayutils/traits/ConcatTrait.php | 1 + .../lib/arrayutils/traits/IterationTrait.php | 1 + .../lib/arrayutils/traits/SearchTrait.php | 1 + .../lib/arrayutils/traits/SetTrait.php | 1 + .../lib/arrayutils/traits/ShapeTrait.php | 1 + .../lib/arrayutils/traits/SortTrait.php | 1 + .../lib/arrayutils/traits/StackTrait.php | 1 + tests/unit/ArrayUtilsTest.php | 23 + tools/generate-traits.py | 520 ++++++++++++++++++ 9 files changed, 550 insertions(+) create mode 100644 tools/generate-traits.py diff --git a/src/kim/present/lib/arrayutils/traits/ConcatTrait.php b/src/kim/present/lib/arrayutils/traits/ConcatTrait.php index df9246a..e7b6c2c 100644 --- a/src/kim/present/lib/arrayutils/traits/ConcatTrait.php +++ b/src/kim/present/lib/arrayutils/traits/ConcatTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/src/kim/present/lib/arrayutils/traits/IterationTrait.php b/src/kim/present/lib/arrayutils/traits/IterationTrait.php index 9ec3f81..da1edc0 100644 --- a/src/kim/present/lib/arrayutils/traits/IterationTrait.php +++ b/src/kim/present/lib/arrayutils/traits/IterationTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/src/kim/present/lib/arrayutils/traits/SearchTrait.php b/src/kim/present/lib/arrayutils/traits/SearchTrait.php index 53ac111..aa677eb 100644 --- a/src/kim/present/lib/arrayutils/traits/SearchTrait.php +++ b/src/kim/present/lib/arrayutils/traits/SearchTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/src/kim/present/lib/arrayutils/traits/SetTrait.php b/src/kim/present/lib/arrayutils/traits/SetTrait.php index 87a046a..d788b09 100644 --- a/src/kim/present/lib/arrayutils/traits/SetTrait.php +++ b/src/kim/present/lib/arrayutils/traits/SetTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/src/kim/present/lib/arrayutils/traits/ShapeTrait.php b/src/kim/present/lib/arrayutils/traits/ShapeTrait.php index b243c97..bab5708 100644 --- a/src/kim/present/lib/arrayutils/traits/ShapeTrait.php +++ b/src/kim/present/lib/arrayutils/traits/ShapeTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/src/kim/present/lib/arrayutils/traits/SortTrait.php b/src/kim/present/lib/arrayutils/traits/SortTrait.php index 2b19089..f22ad9a 100644 --- a/src/kim/present/lib/arrayutils/traits/SortTrait.php +++ b/src/kim/present/lib/arrayutils/traits/SortTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/src/kim/present/lib/arrayutils/traits/StackTrait.php b/src/kim/present/lib/arrayutils/traits/StackTrait.php index d204735..1191aa6 100644 --- a/src/kim/present/lib/arrayutils/traits/StackTrait.php +++ b/src/kim/present/lib/arrayutils/traits/StackTrait.php @@ -21,6 +21,7 @@ * * @noinspection PhpUnused * @noinspection PhpDocSignatureIsNotCompleteInspection + * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead */ declare(strict_types=1); diff --git a/tests/unit/ArrayUtilsTest.php b/tests/unit/ArrayUtilsTest.php index cfaaaea..4c21559 100644 --- a/tests/unit/ArrayUtilsTest.php +++ b/tests/unit/ArrayUtilsTest.php @@ -47,6 +47,29 @@ public function testStackMethods() : void{ $this->assertSame([1, 2], $array); } + public function testPopAndShiftRemoveFromTheInstance() : void{ + $utils = new ArrayUtils(["a" => 1, "b" => 2, 5 => 3, 9 => 4]); + $this->assertSame(4, $utils->pop()); + $this->assertSame(3, count($utils)); + $this->assertSame(["a" => 1, "b" => 2, 5 => 3], $utils->getArrayCopy()); + $this->assertSame(1, $utils->shift()); + $this->assertSame(2, count($utils)); + $this->assertSame(["b" => 2, 0 => 3], $utils->getArrayCopy()); + $this->assertSame(3, $utils->pop()); + $this->assertSame(2, $utils->shift()); + $this->assertSame(0, count($utils)); + $this->assertNull($utils->pop()); + $this->assertNull($utils->shift()); + $this->assertSame([], $utils->getArrayCopy()); + + $utils = new ArrayUtils([1, 2, 3]); + $this->assertSame(1, $utils->shift()); + $this->assertSame([2, 3], $utils->getArrayCopy()); + $this->assertTrue(isset($utils[0])); + $this->assertFalse(isset($utils[2])); + $this->assertSame([2, 3], iterator_to_array($utils)); + } + public function testSearchMethods() : void{ $array = ["a" => 1, "b" => null, "c" => 3]; $this->assertTrue(ArrayUtils::keyExistsFrom($array, "b")); diff --git a/tools/generate-traits.py b/tools/generate-traits.py new file mode 100644 index 0000000..df79465 --- /dev/null +++ b/tools/generate-traits.py @@ -0,0 +1,520 @@ +#!/usr/bin/env python3 +""" +Generates the method traits in src/kim/present/lib/arrayutils/traits/ + +Every ArrayUtils method exists in up to four variants (name, nameAs, nameFrom, nameFromAs) that only differ in +where the array comes from and what is returned. Writing them by hand is repetitive and error-prone, +so they are described once below (see spec()) and generated. + +Usage: + python3 tools/generate-traits.py Rewrite the trait files + python3 tools/generate-traits.py --check Exit with 1 if the trait files are not up to date (used by CI) + +To change a method, edit the spec in this file and run the script. Do not edit the generated trait files by hand. +""" +import os +import re +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +SRC = os.path.join(ROOT, "src", "kim", "present", "lib", "arrayutils") +OUT = os.path.join(SRC, "traits") +DOC = "https://arrayutils.docs.present.kim/methods/" + +with open(os.path.join(SRC, "ArrayUtils.php"), encoding="utf-8") as f: + HEADER = f.read().split("declare(strict_types=1);")[0] +HEADER = re.sub(r"\n \* @noinspection.*", "", HEADER).rstrip() +assert HEADER.endswith("*/") +HEADER = HEADER[:-2].rstrip() + +FUNCS = """array_filter array_key_last array_map array_reduce array_chunk array_column array_key_exists array_combine array_count_values array_diff array_diff_assoc array_diff_key +array_fill_keys array_flip array_intersect array_intersect_assoc array_intersect_key array_keys array_merge array_pad +array_pop array_push array_replace_recursive array_reverse array_search array_shift array_slice array_splice array_sum +array_unique array_unshift array_values count end implode in_array is_array key ksort max min random_int sort uksort +usort""".split() + +# kind: "A" => array result (instance/As/From/FromAs), "V" => value result (instance/From) +# expr: single expression using $a (the input array); core: statement body using $array +# ret: return type (V only) +M = {} +def indent(text, n): + return "\n".join((" " * n + l) if l.strip() else "" for l in text.split("\n")) + +def expand(core, tiers): + """Duplicates a callback loop per callback arity so the callback only receives the arguments it declares. + tiers: [(max arity, number of arguments, native fast path code or None)]""" + m = re.search(r"\$callback\(([^)]*)\)", core) + args = [a.strip() for a in m.group(1).split(",")] + def body(n): + return re.sub(r"\$callback\(([^)]*)\)", lambda mm: "$callback(%s)" % ", ".join(args[:n]), core).strip("\n") + out = ["$arity = self::callbackArity($callback);"] + for max_arity, n, fast in tiers: + out.append("if($arity <= %d){\n%s\n}" % (max_arity, indent(fast if fast else body(n), 4))) + out.append(core.strip("\n")) + return "\n".join(out) + +def spec(trait, name, summary, link, sig=(), kind="A", expr=None, core=None, ret="", extra_doc="", tiers=None): + if tiers: + core = expand(core, tiers) + M.setdefault(trait, []).append(dict(name=name, summary=summary, link=link, sig=list(sig), kind=kind, + expr=expr, core=core, ret=ret, extra_doc=extra_doc)) + +def raw(trait, text): + M.setdefault(trait, []).append(dict(raw=text)) + +CB = ("callable $callback", "$callback") +IT = ("iterable ...$iterables", "...$iterables") +VALS = ("...$values", "...$values") +EACH = "foreach($array as $key => $value){" + +# ---------------------------------------------------------------- ConcatTrait +T = "ConcatTrait" +spec(T, "concat", "Merge one or more arrays", "c/concat", [VALS], expr="array_merge($a, ...self::mapToArray($values))") +spec(T, "concatSoft", "All similar to concat(), but not overwrite existing keys", "c/concat/soft", [VALS], core=""" +foreach($values as $value){ + $array += (array) $value; +} +return $array;""") +spec(T, "merge", "Alias of concat()", "c/concat", [VALS], expr="array_merge($a, ...self::mapToArray($values))") +spec(T, "mergeSoft", "Alias of concatSoft()", "c/concat/soft", [VALS], core="return self::concatSoftFromAs($array, ...$values);") +spec(T, "replace", "Replaces elements from passed arrays into the first array", "c/replace", [IT], + expr="array_replace_recursive($a, ...self::mapToArray($iterables))") +spec(T, "flat", "Returns a new array with all sub-array elements concatenated into it recursively up to the specified depth", + "c/flat", [("int $dept = 1", "$dept")], core=""" +if($dept <= 0){ + return $array; +} +$parts = []; +foreach($array as $value){ + $parts[] = is_array($value) ? self::flatFromAs($value, $dept - 1) : (array) $value; +} +return $parts ? array_merge(...$parts) : [];""") +spec(T, "flatMap", "Returns a new array formed by applying $callback function and then flattening the result by one level", + "c/flat/map", [CB], core=""" +$parts = []; +foreach($array as $key => $value){ + $parts[] = (array) $callback($value, $key, $array); +} +return $parts ? array_merge(...$parts) : [];""", tiers=[(1, 1, None), (2, 2, None)]) + +# ---------------------------------------------------------------- SetTrait +T = "SetTrait" +def setop(name, fn, summary, link): + spec(T, name, summary, link, [IT], expr="$iterables ? %s($a, ...self::mapToArray($iterables)) : $a" % fn) +setop("diff", "array_diff", "Computes the difference of arrays", "c/diff") +setop("diffAssoc", "array_diff_assoc", "All similar to diff(), but this applies with additional index check", "c/diff/assoc") +setop("diffKey", "array_diff_key", "All similar to diff(), but this applies to keys", "c/diff/key") +setop("intersect", "array_intersect", "Computes the intersection of arrays", "c/intersect") +setop("intersectAssoc", "array_intersect_assoc", "All similar to intersect(), but this applies to both keys and values", "c/intersect/assoc") +setop("intersectKey", "array_intersect_key", "All similar to intersect(), but this applies to keys", "c/intersect/key") +spec(T, "unique", "Removes duplicate values from an array", "c/unique", [("int $sortFlags = SORT_STRING", "$sortFlags")], + expr="array_unique($a, $sortFlags)") +spec(T, "countValues", "Counts all the values of an array", "c/count-values", expr="array_count_values($a)") + +# ---------------------------------------------------------------- IterationTrait +T = "IterationTrait" +spec(T, "map", "Applies the callback to the values of the given arrays", "c/map", [CB], core=""" +$result = []; +%s + $result[$key] = $callback($value, $key, $array); +} +return $result;""" % EACH, tiers=[(1, 1, "return array_map($callback, $array);"), (2, 2, None)]) +spec(T, "mapAssoc", "All similar to map(), but this applies to both keys and values", "c/map/assoc", [CB], core=""" +$result = []; +%s + [$newKey, $newValue] = $callback($value, $key, $array); + $result[$newKey] = $newValue; +} +return $result;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "mapKey", "All similar to map(), but this applies to keys", "c/map/key", [CB], core=""" +$result = []; +%s + $result[$callback($value, $key, $array)] = $value; +} +return $result;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "filter", "Returns a new array with all elements that pass the $callback function", "c/filter", [CB], core=""" +$result = []; +%s + if($callback($value, $key, $array)){ + $result[$key] = $value; + } +} +return $result;""" % EACH, tiers=[(1, 1, "return array_filter($array, $callback);"), (2, 2, "return array_filter($array, $callback, ARRAY_FILTER_USE_BOTH);")]) +spec(T, "forEach", "Executes a $callback function once for each array element", "c/for-each", [CB], core=""" +%s + $callback($value, $key, $array); +} +return $array;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "every", "Tests whether all elements pass the $callback function", "g/every", [CB], kind="V", ret="bool", core=""" +%s + if(!$callback($value, $key, $array)){ + return false; + } +} +return true;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "some", "Tests whether least one element pass the $callback function", "g/some", [CB], kind="V", ret="bool", core=""" +%s + if($callback($value, $key, $array)){ + return true; + } +} +return false;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "reduce", "Executes a reducer function on each element of the array, resulting in single output value", "g/reduce", + [CB, ("$initialValue = null", "$initialValue")], kind="V", core=""" +$currentValue = $initialValue; +%s + $currentValue = $callback($currentValue, $value, $key, $array); +} +return $currentValue;""" % EACH, tiers=[(2, 2, "return array_reduce($array, $callback, $initialValue);"), (3, 3, None)]) +spec(T, "reduceRight", "All similar to reduce(), but reverse order", "g/reduce/right", + [CB, ("$initialValue = null", "$initialValue")], kind="V", core=""" +$currentValue = $initialValue; +foreach(array_reverse($array, true) as $key => $value){ + $currentValue = $callback($currentValue, $value, $key, $array); +} +return $currentValue;""", tiers=[(2, 2, "return array_reduce(array_reverse($array), $callback, $initialValue);"), (3, 3, None)]) +spec(T, "sum", "Calculate the sum of values in an array", "g/sum", kind="V", ret="int|float", expr="array_sum($a)") + +# ---------------------------------------------------------------- SearchTrait +T = "SearchTrait" +START = ("int $start = 0", "$start") +NEEDLE = ("$needle", "$needle") +CLAMP = """if($start !== 0){ + $count = count($array); + $array = array_slice($array, $start < 0 ? max($count + $start, 0) : min($start, $count), null, true); +}""" +spec(T, "includes", "Tests whether an array includes a $needle", "g/includes", [NEEDLE, START], kind="V", ret="bool", core=CLAMP + """ +return in_array($needle, $array, true);""") +spec(T, "indexOf", "Returns the first index at which a given element can be found in the array", "g/index-of", + [NEEDLE, START], kind="V", ret="int|string|null", core=CLAMP + """ +$key = array_search($needle, $array, true); +return $key === false ? null : $key;""") +spec(T, "search", "Alias of indexOf()", "g/index-of", [NEEDLE, START], kind="V", ret="int|string|null", + core="return self::indexOfFrom($array, $needle, $start);") +spec(T, "keyExists", "Tests whether the $key exists in the array", "g/key-exists", [("$key", "$key")], + kind="V", ret="bool", core="return array_key_exists($key, $array);") +spec(T, "find", "Returns the value of the first element that that pass the $callback function", "g/find", [CB], kind="V", core=""" +%s + if($callback($value, $key, $array)){ + return $value; + } +} +return null;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "findIndex", "Returns the key of the first element that that pass the $callback function", "g/find/index", [CB], + kind="V", ret="int|string|null", core=""" +%s + if($callback($value, $key, $array)){ + return $key; + } +} +return null;""" % EACH, tiers=[(1, 1, None), (2, 2, None)]) +spec(T, "first", "Returns the first value of an array, or null if it is empty", "g/first", kind="V", core=""" +foreach($array as $value){ + return $value; +} +return null;""") +spec(T, "keyFirst", "Gets the first key of an array", "g/first/key", kind="V", ret="int|string|null", core=""" +foreach($array as $key => $_){ + return $key; +} +return null;""") +spec(T, "last", "Returns the last value of an array, or null if it is empty", "g/last", kind="V", core=""" +if(\\PHP_VERSION_ID >= 70300){ + $key = array_key_last($array); + return $key === null ? null : $array[$key]; +} +return $array ? end($array) : null;""") +spec(T, "keyLast", "Gets the last key of an array", "g/last/key", kind="V", ret="int|string|null", core=""" +if(\\PHP_VERSION_ID >= 70300){ + return array_key_last($array); +} +if(!$array){ + return null; +} +end($array); +return key($array);""") +spec(T, "random", "Returns the random value of an array, or null if it is empty", "g/random", kind="V", core=""" +$count = count($array); +if($count === 0){ + return null; +} +try{ + $index = random_int(0, $count - 1); +}catch(Exception $_){ + return null; +} +return array_values($array)[$index];""") +spec(T, "keyRandom", "Gets the random key of an array", "g/random/key", kind="V", ret="int|string|null", core=""" +$count = count($array); +if($count === 0){ + return null; +} +try{ + $index = random_int(0, $count - 1); +}catch(Exception $_){ + return null; +} +return array_keys($array)[$index];""") + +# ---------------------------------------------------------------- SortTrait +T = "SortTrait" +spec(T, "sort", "Sort an array by values using a $callback function or default sort function\n * If $callback is null, run sort(), else run usort()", + "c/sort", [("?callable $callback = null", "$callback")], core=""" +if($callback === null){ + sort($array); +}else{ + usort($array, $callback); +} +return $array;""") +spec(T, "sortKey", "Sort an array by keys using a $callback function or default sort function\n * If $callback is null, run ksort(), else run uksort()", + "c/sort/key", [("?callable $callback = null", "$callback")], core=""" +if($callback === null){ + ksort($array); +}else{ + uksort($array, $callback); +} +return $array;""") +spec(T, "reverse", "Returns an array with elements in reverse order", "c/reverse", [("bool $preserveKeys = false", "$preserveKeys")], + expr="array_reverse($a, $preserveKeys)") + +# ---------------------------------------------------------------- StackTrait +T = "StackTrait" +spec(T, "push", "Push elements onto the end of array", "c/push", [VALS], core=""" +if($values){ + array_push($array, ...$values); +} +return $array;""") +spec(T, "unshift", "Push elements onto the start of array", "c/unshift", [VALS], core=""" +if($values){ + array_unshift($array, ...$values); + return $array; +} + +//Without values, array_unshift() only renumbers the integer keys (calling it without values is invalid before PHP 7.3) +$result = []; +foreach($array as $key => $value){ + if(is_int($key)){ + $result[] = $value; + }else{ + $result[$key] = $value; + } +} +return $result;""") +raw(T, ''' /** + * Removes the last element and returns that element + * + * @link https://arrayutils.docs.present.kim/methods/g/pop + */ + public function pop(){ + $array = $this->getArrayCopy(); + $value = array_pop($array); + $this->exchangeArray($array); + return $value; + } + + /** + * Same as pop(), but operates on a copy of the given iterable (the given iterable is not modified) + * + * @link https://arrayutils.docs.present.kim/methods/g/pop + */ + public static function popFrom(iterable $from){ + $array = is_array($from) ? $from : self::toArray($from); + return array_pop($array); + } + + /** + * Removes the first element and returns that element + * + * @link https://arrayutils.docs.present.kim/methods/g/shift + */ + public function shift(){ + $array = $this->getArrayCopy(); + $value = array_shift($array); + $this->exchangeArray($array); + return $value; + } + + /** + * Same as shift(), but operates on a copy of the given iterable (the given iterable is not modified) + * + * @link https://arrayutils.docs.present.kim/methods/g/shift + */ + public static function shiftFrom(iterable $from){ + $array = is_array($from) ? $from : self::toArray($from); + return array_shift($array); + } + + /** + * Remove a portion of the array and replace it with something else + * If $length is null, removes everything from $offset to the end + * + * @return array The removed elements + * @link https://arrayutils.docs.present.kim/methods/g/splice + */ + public function splice(int $offset, ?int $length = null, ...$replacement) : array{ + $array = $this->getArrayCopy(); + $removed = array_splice($array, $offset, $length ?? count($array), $replacement); + $this->exchangeArray($array); + return $removed; + } + + /** + * Same as splice(), but operates on a copy of the given iterable (the given iterable is not modified) + * + * @return array The removed elements + * @link https://arrayutils.docs.present.kim/methods/g/splice + */ + public static function spliceFrom(iterable $from, int $offset, ?int $length = null, ...$replacement) : array{ + $array = is_array($from) ? $from : self::toArray($from); + return array_splice($array, $offset, $length ?? count($array), $replacement); + } +''') + +# ---------------------------------------------------------------- ShapeTrait +T = "ShapeTrait" +spec(T, "chunk", "Split an array into chunks", "c/chunk", [("int $size", "$size"), ("bool $preserveKeys = false", "$preserveKeys")], + expr="array_chunk($a, $size, $preserveKeys)") +spec(T, "column", "Returns the values from a single column in the input array", "c/column", + [("$valueKey", "$valueKey"), ("$indexKey = null", "$indexKey")], expr="array_column($a, $valueKey, $indexKey)") +spec(T, "combine", "Creates an array by using one array for keys and another for its values\n * If $valueArray is null, uses the array itself", + "c/combine", [("?iterable $valueArray = null", "$valueArray")], expr="array_combine($a, (array) ($valueArray ?? $a))") +spec(T, "fill", "Changes all elements in an array to a provided value, from a start index to an end index", "c/fill", + [("$value", "$value"), ("int $start = 0", "$start"), ("?int $end = null", "$end")], core=""" +$keys = array_keys($array); +[$i, $max] = self::resolveRange(count($keys), $start, $end ?? PHP_INT_MAX); +for(; $i < $max; ++$i){ + $array[$keys[$i]] = $value; +} +return $array;""") +spec(T, "fillKeys", "Fill an array with values, specifying keys", "c/fill/keys", [("$value", "$value")], + expr="array_fill_keys($a, $value)") +spec(T, "flip", "Exchanges all keys with their associated values in an array", "c/flip", expr="array_flip($a)") +spec(T, "keys", "Returns all the keys of an array", "c/keys", expr="array_keys($a)") +spec(T, "values", "Returns all the values of an array", "c/values", expr="array_values($a)") +spec(T, "pad", "Pad array to the specified length with a value", "c/pad", [("int $size", "$size"), ("$value", "$value")], + expr="array_pad($a, $size, $value)") +spec(T, "slice", "Returns an array with selected from start to end\n * Extract a slice of the array", "c/slice", + [("int $start = 0", "$start"), ("?int $end = null", "$end"), ("bool $preserveKeys = false", "$preserveKeys")], core=""" +[$i, $max] = self::resolveRange(count($array), $start, $end ?? PHP_INT_MAX); +if($max <= $i){ + return []; +} +$result = array_slice($array, $i, $max - $i, true); +return $preserveKeys ? $result : array_values($result);""") +spec(T, "join", "Join array elements with a string. You can specify a suffix and prefix", "g/join", + [('string $glue = ","', "$glue"), ('string $prefix = ""', "$prefix"), ('string $suffix = ""', "$suffix")], + kind="V", ret="string", expr="$prefix . implode($glue, $a) . $suffix") +raw(T, ''' /** + * Converts the $start and $end indexes (negative values count from the end) to an offset range of $count elements + * + * @return int[] [first index, last index + 1] + */ + private static function resolveRange(int $count, int $start, int $end) : array{ + return [ + $start < 0 ? max($count + $start, 0) : min($start, $count), + $end < 0 ? max($count + $end, 0) : min($end, $count) + ]; + } +''') + +# ---------------------------------------------------------------- rendering +def sigdecl(sig): return ", ".join(d for d, _ in sig) +def sigcall(sig): return ", ".join(c for _, c in sig) + +def render_expr(expr, source): + """returns (prefix statements, return expression)""" + if len(re.findall(r"\$a\b", expr)) > 1: + return "$a = %s;\n" % source, expr + return "", re.sub(r"\$a\b", lambda m: source, expr) + +def method(fn_name, decl, ret, body, doc, static=False): + r = (" : " + ret) if ret else "" + s = "static " if static else "" + return " /**\n * %s\n */\n public %sfunction %s(%s)%s{\n%s\n }\n" % (doc, s, fn_name, decl, r, indent(body, 8)) + +def gen(m): + n, sig, kind = m["name"], m["sig"], m["kind"] + decl, call = sigdecl(sig), sigcall(sig) + fdecl = "iterable $from" + (", " + decl if decl else "") + link = "\n *\n * @link " + DOC + m["link"] + out = [] + if kind == "A": + if m["expr"]: + p1, e1 = render_expr(m["expr"], "$this->getArrayCopy()") + p2, e2 = render_expr(m["expr"], "(is_array($from) ? $from : self::toArray($from))") + b_inst = p1 + "$this->exchangeArray(%s);\nreturn $this;" % e1 + b_as = p1 + "return %s;" % e1 + b_fromas = p2 + "return %s;" % e2 + b_from = p2 + "$utils = self::blank();\n$utils->exchangeArray(%s);\nreturn $utils;" % e2 + else: + c = "self::%sFromAs($this->getArrayCopy(), %s)" % (n, call) if call else "self::%sFromAs($this->getArrayCopy())" % n + b_inst = "$this->exchangeArray(%s);\nreturn $this;" % c + b_as = "return %s;" % c + cf = "self::%sFromAs($from, %s)" % (n, call) if call else "self::%sFromAs($from)" % n + b_from = "$utils = self::blank();\n$utils->exchangeArray(%s);\nreturn $utils;" % cf + b_fromas = "$array = is_array($from) ? $from : self::toArray($from);\n" + m["core"].strip("\n") + out.append(method(n, decl, "ArrayUtils", b_inst, m["summary"] + link)) + out.append(method(n + "As", decl, "array", b_as, "Same as %s(), but returns a plain array and leaves the instance unchanged" % n)) + out.append(method(n + "From", fdecl, "ArrayUtils", b_from, "Same as %s(), but operates on the given iterable and returns a new ArrayUtils" % n, True)) + out.append(method(n + "FromAs", fdecl, "array", b_fromas, "Same as %sFrom(), but returns a plain array" % n, True)) + else: + ret = m["ret"] + phpret = ret if ret in ("bool", "string") else "" + doc_ret = "" if phpret or not ret else "\n *\n * @return " + ret + if m["expr"]: + p1, e1 = render_expr(m["expr"], "$this->getArrayCopy()") + p2, e2 = render_expr(m["expr"], "(is_array($from) ? $from : self::toArray($from))") + b_inst = p1 + "return %s;" % e1 + b_from = p2 + "return %s;" % e2 + else: + cf = "self::%sFrom($this->getArrayCopy(), %s)" % (n, call) if call else "self::%sFrom($this->getArrayCopy())" % n + b_inst = "return %s;" % cf + b_from = "$array = is_array($from) ? $from : self::toArray($from);\n" + m["core"].strip("\n") + out.append(method(n, decl, phpret, b_inst, m["summary"] + doc_ret + link)) + out.append(method(n + "From", fdecl, phpret, b_from, "Same as %s(), but operates on the given iterable" % n + doc_ret, True)) + return "\n".join(out) + +DESC = { + "ConcatTrait": "Methods that join arrays together (concat, merge, replace, flat)", + "SetTrait": "Methods that compare arrays or collapse duplicates (diff, intersect, unique, countValues)", + "IterationTrait": "Methods that walk every element with a callback (map, filter, forEach, reduce, every, some)", + "SearchTrait": "Methods that look up a single element or key (includes, indexOf, find, first, last, random)", + "SortTrait": "Methods that reorder the elements (sort, sortKey, reverse)", + "StackTrait": "Methods that add or remove elements at a position (push, pop, shift, unshift, splice)", + "ShapeTrait": "Methods that change the shape of the array (chunk, column, combine, fill, flip, keys, values, pad, slice, join)", +} +GENERATED = " * @generated by tools/generate-traits.py - do not edit by hand, edit the spec in that script instead" + +outputs = {} +for trait, items in M.items(): + body = "\n".join(it["raw"] if "raw" in it else gen(it) for it in items) + funcs = sorted(f for f in FUNCS if re.search(r"(?:$])%s\(" % f, body)) + uses = ["use kim\\present\\lib\\arrayutils\\ArrayUtils;"] + (["use Exception;"] if "Exception" in body else []) + uses += ["use const ARRAY_FILTER_USE_BOTH;"] if "ARRAY_FILTER_USE_BOTH" in body else [] + uses += ["use function %s;" % f for f in funcs] + text = HEADER.rstrip("\n") + "\n * @noinspection PhpUnused\n * @noinspection PhpDocSignatureIsNotCompleteInspection\n" + GENERATED + "\n */\n\ndeclare(strict_types=1);\n\nnamespace kim\\present\\lib\\arrayutils\\traits;\n\n" + text += "\n".join(uses) + ("\n\n" if uses else "") + text += "/**\n * %s\n *\n * Requires ArrayUtils::toArray(), ArrayUtils::mapToArray(), ArrayUtils::blank() and ArrayUtils::callbackArity() of the class using this trait\n */\ntrait %s{\n" % (DESC[trait], trait) + text += body.rstrip("\n") + "\n}\n" + outputs[os.path.join(OUT, trait + ".php")] = text + +if "--check" in sys.argv: + stale = [] + for path, text in outputs.items(): + try: + with open(path, encoding="utf-8", newline="") as f: + current = f.read() + except FileNotFoundError: + current = None + if current != text: + stale.append(os.path.relpath(path, ROOT)) + if stale: + print("Out of date, run `python3 tools/generate-traits.py`:\n " + "\n ".join(stale)) + sys.exit(1) + print("Traits are up to date") +else: + os.makedirs(OUT, exist_ok=True) + for path, text in outputs.items(): + with open(path, "w", encoding="utf-8", newline="") as f: + f.write(text) + print("Generated %d traits" % len(outputs))