diff --git a/README.md b/README.md index 62610db..c56de92 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ $hashedPassword = $config->get('db.password'); ### 5. Hooked Collection ```php -use Infocyph\ArrayKit\Functional\HookedCollection; +use Infocyph\ArrayKit\Collection\HookedCollection; $hooked = new HookedCollection(['name' => 'Alice']); diff --git a/src/Array/ArrayMulti.php b/src/Array/ArrayMulti.php index 07160c9..4048e6f 100644 --- a/src/Array/ArrayMulti.php +++ b/src/Array/ArrayMulti.php @@ -7,61 +7,64 @@ use RecursiveArrayIterator; use RecursiveIteratorIterator; -/** - * Class ArrayMulti - * - * Provides operations for multidimensional arrays, including: - * collapsing, flattening, recursive sorting, and more advanced - * 2D or multidimensional filtering. - */ class ArrayMulti { /** - * Get a subset of each sub-array from the given 2D (or multidimensional) array - * based on specified keys. + * Select only certain keys from a multidimensional array. * - * @param array $array A multidimensional array (array of arrays) - * @param array|string $keys One or multiple keys to keep - * @return array An array of arrays, each containing only the specified keys + * This method is the multidimensional equivalent of ArraySingle::only. + * + * @param array $array the multidimensional array to select from + * @param array|string $keys the keys to select + * @return array a new array with the selected keys */ public static function only(array $array, array|string $keys): array { $result = []; - $pick = array_flip((array) $keys); + $pick = array_flip((array)$keys); foreach ($array as $item) { if (is_array($item)) { $result[] = array_intersect_key($item, $pick); } } - return $result; } + /** - * Collapse an array of arrays into a single (1D) array. + * Collapses a multidimensional array into a single-dimensional array. + * + * This method takes a multidimensional array and merges all its + * sub-arrays into a single-level array. Only one level of the + * array is collapsed, so nested arrays within sub-arrays will + * remain unchanged. * - * @param array $array A multi-dimensional array - * @return array The collapsed 1D array + * @param array $array The multidimensional array to collapse. + * @return array A single-dimensional array with all sub-array elements. */ public static function collapse(array $array): array { $results = []; - foreach ($array as $values) { if (is_array($values)) { $results = array_merge($results, $values); } } - return $results; } + /** - * Get the maximum depth of a multidimensional array. + * Determine the depth of a multidimensional array. * - * @param array $array The array to measure - * @return int The maximum nesting depth + * The depth is the level of nesting of the array, i.e. the + * number of levels of arrays that are nested within one + * another. The outermost level is 1, and each nested level + * increments the depth by 1. + * + * @param array $array The multidimensional array to determine the depth of. + * @return int The depth of the array. */ public static function depth(array $array): int { @@ -69,33 +72,34 @@ public static function depth(array $array): int return 0; } - $depth = 0; + $depth = 0; $iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array)); foreach ($iterator as $unused) { - $depth = max($iterator->getDepth(), $depth); + $depth = max($depth, $iterator->getDepth()); } - - // getDepth() is zero-based, so add 1 to get the actual depth - return $depth + 1; + return $depth + 1; // zero-based => plus one } + /** - * Recursively flatten a multidimensional array into a single-level array. + * Recursively flatten a multidimensional array to a specified depth. + * + * This method takes a multidimensional array and reduces it to a single-level + * array up to the specified depth. If no depth is specified or if it is set + * to \INF, the array will be completely flattened. * - * @param array $array The array to flatten - * @param float|int $depth The maximum depth to flatten (default: INF, meaning fully flatten) - * @return array Flattened array + * @param array $array The multidimensional array to flatten. + * @param float|int $depth The maximum depth to flatten. Defaults to infinite. + * @return array A flattened array up to the specified depth. */ - public static function flatten(array $array, float|int $depth = INF): array + public static function flatten(array $array, float|int $depth = \INF): array { $result = []; - foreach ($array as $item) { if (!is_array($item)) { $result[] = $item; } else { - // If depth is 1, only flatten one level $values = ($depth === 1) ? array_values($item) : static::flatten($item, $depth - 1); @@ -105,36 +109,43 @@ public static function flatten(array $array, float|int $depth = INF): array } } } - return $result; } + /** - * Flatten an array into a single level but preserve keys (recursive). + * Flatten the array into a single level but preserve keys. * - * @param array $array The array to flatten - * @return array The flattened array with keys + * This method takes a multidimensional array and reduces it to a single-level + * array, but preserves all keys. The resulting array will have the same keys + * as the original array, but with all nested arrays flattened to the same + * level. + * + * @param array $array The multidimensional array to flatten. + * @return array A flattened array with all nested arrays flattened to the same level. */ public static function flattenByKey(array $array): array { return iterator_to_array( new RecursiveIteratorIterator(new RecursiveArrayIterator($array)), - false + false, ); } + /** - * Recursively sort an array by its keys and values. + * Recursively sort a multidimensional array by keys/values. * - * - Uses ksort/krsort if the array is associative. - * - Uses sort/rsort if the array is sequential. + * This method takes a multidimensional array and recursively sorts it by + * keys or values. The sorting options and direction are determined by the + * $options and $descending parameters respectively. * - * @param array $array The array to sort - * @param int $options Sorting options - * @param bool $descending If true, sort in descending order - * @return array The sorted array + * @param array $array The multidimensional array to sort. + * @param int $options The sorting options. Defaults to SORT_REGULAR. + * @param bool $descending Whether to sort in descending order. Defaults to false. + * @return array The sorted array. */ - public static function sortRecursive(array $array, int $options = SORT_REGULAR, bool $descending = false): array + public static function sortRecursive(array $array, int $options = \SORT_REGULAR, bool $descending = false): array { foreach ($array as &$value) { if (is_array($value)) { @@ -151,23 +162,23 @@ public static function sortRecursive(array $array, int $options = SORT_REGULAR, ? rsort($array, $options) : sort($array, $options); } - return $array; } + /** - * Get the first element in a 2D (or multidimensional) array. - * Optionally pass a callback to find the first item that matches. + * Return the first item in a 2D array, or single-dim array, depending on usage. + * If a callback is provided, return the first item that matches the callback. + * Otherwise, return the first item in the array. * - * @param array $array A 2D or multi-dimensional array - * @param callable|null $callback If provided, must return true for the first matching item - * @param mixed|null $default Fallback if no item or no match is found - * @return mixed The found item or the default + * @param array $array The array to search in. + * @param callable|null $callback The callback to apply to each element. + * @param mixed $default The default value to return if the array is empty. + * @return mixed The first item in the array, or the default value if empty. */ public static function first(array $array, ?callable $callback = null, mixed $default = null): mixed { if ($callback === null) { - // Return the first item in the array if it exists return empty($array) ? $default : reset($array); } @@ -176,203 +187,593 @@ public static function first(array $array, ?callable $callback = null, mixed $de return $value; } } - return $default; } + /** - * Get the last element in a 2D (or multidimensional) array. - * Optionally pass a callback to find the last item that matches. + * Return the last item in a 2D array or single-dim array, depending on usage. + * If a callback is provided, return the last item that matches the callback. + * Otherwise, return the last item in the array. * - * @param array $array A 2D or multi-dimensional array - * @param callable|null $callback If provided, must return true for the last matching item - * @param mixed|null $default Fallback if no match is found - * @return mixed The found item or the default + * @param array $array The array to search in. + * @param callable|null $callback The callback to apply to each element. + * @param mixed $default The default value to return if the array is empty. + * @return mixed The last item in the array, or the default value if empty. */ public static function last(array $array, ?callable $callback = null, mixed $default = null): mixed { if ($callback === null) { - // Return the last item in the array if it exists return empty($array) ? $default : end($array); } - - // Reverse array with preserve_keys = true, to find last matching + // Reverse array with preserve_keys = true: return static::first(array_reverse($array, true), $callback, $default); } + /** - * Filter items such that the value of the given key is between two values. - * (Intended for 2D arrays, e.g. each sub-item has a certain key to compare.) + * Filter a 2D array by a single key's comparison (like "where 'age' between 18 and 65"). * - * @param array $array A 2D or multi-dimensional array - * @param string $key The sub-item key to compare - * @param float|int $from Lower bound (inclusive) - * @param float|int $to Upper bound (inclusive) - * @return array A filtered array of items + * @param array $array The 2D array to filter. + * @param string $key The key in each sub-array to compare. + * @param float|int $from The lower bound of the comparison. + * @param float|int $to The upper bound of the comparison. + * @return array The filtered array. */ public static function between(array $array, string $key, float|int $from, float|int $to): array { - $new = []; - - if (!empty($array)) { - foreach ($array as $index => $item) { - // check that sub-item has the key - if (ArraySingle::exists($item, $key) && - compare($item[$key], $from, '>=') && - compare($item[$key], $to, '<=') - ) { - $new[$index] = $item; - } - } - } - - return $new; + return array_filter($array, fn ($item) => ArraySingle::exists($item, $key) + && compare($item[$key], $from, '>=') + && compare($item[$key], $to, '<=')); } + /** - * Filter a multidimensional array using a callback. - * Returns only items for which the callback returns true. + * Filter a 2D array by a custom callback function on each row. + * + * If no callback is provided, the method will return the entire array. + * If the array is empty and a default value is provided, that value will be returned. * - * @param array $array The array to filter - * @param callable|null $callback Callback signature: fn($value, $key): bool - * @param mixed|null $default Value returned if callback is null or array is empty - * @return array Filtered array of items + * @param array $array The 2D array to filter. + * @param callable|null $callback The callback function to apply to each element. + * If null, the method will return the entire array. + * @param mixed $default The default value to return if the array is empty. + * @return mixed The filtered array, or the default value if the array is empty. */ public static function whereCallback(array $array, ?callable $callback = null, mixed $default = null): mixed { if ($callback === null) { return empty($array) ? $default : $array; } - - return array_filter($array, fn ($item, $index) => $callback($item, $index), ARRAY_FILTER_USE_BOTH); + return array_filter($array, fn ($item, $index) => $callback($item, $index), \ARRAY_FILTER_USE_BOTH); } + /** - * Return a multidimensional array filtered by a specified condition on one key. - * - If $operator is null, uses "==" by default in 'compare' method. + * Filter a 2D array by a single key's comparison (like "where 'age' > 18"). * - * @param array $array The array to filter (2D or multi-dimensional) - * @param string $key The key to compare - * @param mixed|null $operator Comparison operator or value if $value is null - * @param mixed|null $value The value to compare against - * @return array Filtered array of items + * If the third argument is omitted, the second argument is treated as the value to compare. + * If the third argument is provided, it is used as the operator for the comparison. + * + * @param array $array The 2D array to filter. + * @param string $key The key in each sub-array to compare. + * @param mixed $operator The operator to use for the comparison. If null, the second argument is treated as the value to compare. + * @param mixed $value The value to compare. + * @return array The filtered array. */ public static function where(array $array, string $key, mixed $operator = null, mixed $value = null): array { - // If only two arguments are passed, treat it as $operator => $value + // If only 2 args, treat second as $value if ($value === null && $operator !== null) { - $value = $operator; + $value = $operator; $operator = null; } - $new = []; - if (!empty($array)) { - foreach ($array as $index => $item) { - if (ArraySingle::exists($item, $key) - && compare($item[$key], $value, $operator)) { - $new[$index] = $item; - } + return array_filter($array, fn ($item) => ArraySingle::exists($item, $key) && compare($item[$key], $value, $operator)); + } + + /** + * Break a 2D array into smaller chunks of a specified size. + * + * This function splits the input array into multiple smaller arrays, each + * containing up to the specified number of elements. If the specified size + * is less than or equal to zero, the entire array is returned as a single chunk. + * + * @param array $array The array to be chunked. + * @param int $size The size of each chunk. + * @param bool $preserveKeys Whether to preserve the keys in the chunks. + * + * @return array An array of arrays, each representing a chunk of the original array. + */ + public static function chunk(array $array, int $size, bool $preserveKeys = false): array + { + if ($size <= 0) { + return [$array]; + } + return array_chunk($array, $size, $preserveKeys); + } + + + /** + * Apply a callback to each row in the array, optionally preserving keys. + * + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return the value to be used + * in the resulting array. + * + * @param array $array The array to be mapped over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The array with each element transformed by the callback. + */ + public static function map(array $array, callable $callback): array + { + $results = []; + foreach ($array as $key => $row) { + $results[$key] = $callback($row, $key); + } + return $results; + } + + + /** + * Execute a callback on each item in the array, returning the original array. + * + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return a value that can be + * evaluated to boolean. If the callback returns false, the iteration is + * broken. Otherwise, the iteration continues. + * + * @param array $array The array to be iterated over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The original array. + */ + public static function each(array $array, callable $callback): array + { + foreach ($array as $key => $row) { + if ($callback($row, $key) === false) { + break; } } + return $array; + } - return $new; + + /** + * Reduce an array to a single value using a callback function. + * + * The callback function receives three arguments: the accumulator, + * the current array value, and the current array key. It should return + * the updated accumulator value. + * + * @param array $array The array to reduce. + * @param callable $callback The callback function to apply to each element. + * @param mixed $initial The initial value of the accumulator. + * @return mixed The reduced value. + */ + public static function reduce(array $array, callable $callback, mixed $initial = null): mixed + { + $accumulator = $initial; + foreach ($array as $key => $row) { + $accumulator = $callback($accumulator, $row, $key); + } + return $accumulator; } + /** - * Get an array of items that pass a given test (callback) on a specific column/key. + * Check if the array (of rows) contains at least one row matching a condition * - * If $callback is not a callable, it’s treated as a value to match (==). - * This is for 2D arrays (list of records/rows). + * @param array $array The array to search. + * @param callable $callback The callback to apply to each element. + * @return bool Whether at least one element passed the truth test. + */ + public static function some(array $array, callable $callback): bool + { + foreach ($array as $key => $row) { + if ($callback($row, $key)) { + return true; + } + } + return false; + } + + + /** + * Determine if all rows in a 2D array pass the given truth test. + * + * The callback function receives two arguments: the value of the current + * row and its key. It should return true if the condition is met, or false otherwise. * - * @param array $array The array to filter - * @param string|callable|null $column If string, the sub-item key to check; - * if callable, used as the callback. - * @param callable|mixed $callback The callback or value for matching - * @return array Filtered array of items + * @param array $array The array of rows to evaluate. + * @param callable $callback The callback to apply to each row. + * @return bool Whether all rows passed the truth test. */ - public static function accept(array $array, string|callable $column = null, string|callable $callback = null): array + public static function every(array $array, callable $callback): bool { - if (empty($callback) && !empty($column)) { - $callback = $column; - $column = null; + foreach ($array as $key => $row) { + if (!$callback($row, $key)) { + return false; + } } + return true; + } - $isCallable = is_callable($callback); - return static::filter( - $array, - $column, - static fn ($value) => $isCallable - ? $callback($value) - : $value == $callback - ); + /** + * Determine if the array contains a given value or if a callback function + * returns true for at least one element. + * + * If the second argument is a callable, it is used as a callback function + * that receives the value and key of each element in the array. If the + * callback returns true, the function returns true. + * + * If the second argument is not a callable, it is used as the value to + * search for in the array. The optional third argument determines whether + * to use strict comparison (===) or loose comparison (==). + * + * @param array $array The array to search. + * @param mixed $valueOrCallback The value to search for, or a callable to apply to each element. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). + * @return bool Whether the array contains the given value or whether the callback returned true for at least one element. + */ + public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool + { + if (is_callable($valueOrCallback)) { + return static::some($array, $valueOrCallback); + } + return in_array($valueOrCallback, $array, $strict); } + /** - * Get an array of items that do NOT pass a given test (callback) on a specific column/key. + * Return a new array with all duplicate rows removed. * - * If $callback is not a callable, it’s treated as a value to match (values != $callback). - * This is for 2D arrays (list of records/rows). + * The method takes an array and an optional boolean parameter as arguments. + * If the boolean parameter is not provided, it defaults to false, which means + * loose comparison (==) will be used when checking for duplicate values. + * If the boolean parameter is true, strict comparison (===) will be used. * - * @param array $array The array to filter - * @param string|callable|null $column If string, the sub-item key to check; - * if callable, used as the callback. - * @param callable|mixed $callback The callback or value for matching - * @return array Filtered array of items that do NOT match + * The method iterates over the array, keeping track of values seen so far + * in an array. If a value is seen for the first time, it is added to the + * results array. If a value is seen again, it is skipped. + * If the value is an array itself, it is serialized before being compared. + * + * @param array $array The array to remove duplicates from. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). Defaults to false. + * @return array The array with all duplicate values removed. */ - public static function except(array $array, string|callable $column = null, string|callable $callback = null): array + public static function unique(array $array, bool $strict = false): array { - if (empty($callback) && !empty($column)) { - $callback = $column; - $column = null; + $seen = []; + $results = []; + foreach ($array as $key => $row) { + // If the row is itself an array, we serialize it for comparison: + $compareValue = is_array($row) ? serialize($row) : $row; + if (!in_array($compareValue, $seen, $strict)) { + $seen[] = $compareValue; + $results[$key] = $row; + } } + return $results; + } - $isCallable = is_callable($callback); - return static::filter( - $array, - $column, - static fn ($value) => $isCallable - ? !$callback($value) - : $value != $callback - ); + /** + * Return an array with all values that do not pass the given callback. + * + * The method takes an array and an optional callback as parameters. + * If the callback is not provided, it defaults to `true`, which means the method will return an array with all + * values that are not equal to `true`. + * If the callback is a callable, the method will use it to filter the array. If the callback returns `false` for + * a value, that value will be rejected. + * If the callback is not a callable, the method will use it as the value to compare against. If the value is equal + * to the callback, it will be rejected. + * + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to filter. + * @param mixed $callback The callback to use for filtering, or the value to compare against. Defaults to `true`. + * @return array The filtered array. + */ + public static function reject(array $array, mixed $callback = true): array + { + // Could unify via BaseArrayHelper::doReject($array, $callback). + // Or keep local logic: + if (is_callable($callback)) { + return array_filter($array, fn ($row, $key) => !$callback($row, $key), \ARRAY_FILTER_USE_BOTH); + } + return array_filter($array, fn ($row) => $row != $callback); } + /** - * Filter the array by a specific column/key using a custom callback. - * If no callback is provided, returns all non-null, non-false column values. + * Partition the array into two arrays [passed, failed] based on a callback. * - * This method effectively checks the column's values - * and returns the entire row if that column's value passes the callback. + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * If the callback returns true, the item is added to the "passed" array. + * If the callback returns false, the item is added to the "failed" array. + * The method returns an array with two elements, the first being the "passed" array, + * and the second being the "failed" array. * - * @param array $array The 2D array to filter - * @param string $column The column to inspect - * @param callable|null $callback The function to apply to each column value - * @return array Filtered rows (preserving keys) + * @param array $array The array to partition. + * @param callable $callback The callback to use for partitioning. + * @return array An array with two elements, the first being the "passed" array, and the second being the "failed" array. */ - public static function filter(array $array, string $column, ?callable $callback = null): array + public static function partition(array $array, callable $callback): array { - if (empty($column)) { - return $array; + $passed = []; + $failed = []; + foreach ($array as $key => $row) { + if ($callback($row, $key)) { + $passed[$key] = $row; + } else { + $failed[$key] = $row; + } + } + return [$passed, $failed]; + } + + + /** + * Skip the first $count items of the array and return the remainder. + * + * The method takes two parameters: the array to skip and the number of items to skip. + * It returns an array with the same type of indices as the input array. + * + * @param array $array The array to skip. + * @param int $count The number of items to skip. + * @return array The skipped array. + */ + public static function skip(array $array, int $count): array + { + return array_slice($array, $count, null, true); + } + + + /** + * Skip rows while the callback returns true; once false, keep the remainder. + * + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each row. + * As long as the callback returns true, the row is skipped. + * The first row for which the callback returns false is kept, + * and all subsequent rows are also kept. + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to skip. + * @param callable $callback The callback to use for skipping. + * @return array The skipped array. + */ + public static function skipWhile(array $array, callable $callback): array + { + $result = []; + $skipping = true; + foreach ($array as $key => $row) { + if ($skipping && !$callback($row, $key)) { + $skipping = false; + } + if (!$skipping) { + $result[$key] = $row; + } + } + return $result; + } + + + /** + * Skip rows until the callback returns true, then keep the remainder. + * + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each row. + * As long as the callback returns false, the row is skipped. + * The first row for which the callback returns true is kept, + * and all subsequent rows are also kept. + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to skip. + * @param callable $callback The callback to use for skipping. + * @return array The skipped array. + */ + public static function skipUntil(array $array, callable $callback): array + { + return static::skipWhile($array, fn ($row, $key) => !$callback($row, $key)); + } + + + /** + * Calculate the sum of an array of values, optionally using a key or callback to extract the values to sum. + * + * If no key or callback is provided, the method will add up all numeric values in the array. + * If a key is provided, the method will add up all values in the array that are keyed by that column. + * If a callback is provided, the method will pass each row in the array to the callback and add up the results. + * + * @param array $array The array to sum. + * @param string|callable|null $keyOrCallback The key or callback to use to extract the values to sum. + * @return float|int The sum of the values in the array. + */ + public static function sum(array $array, string|callable|null $keyOrCallback = null): float|int + { + $total = 0; + foreach ($array as $row) { + if ($keyOrCallback === null) { + if (is_numeric($row)) { + $total += $row; + } + } elseif (is_callable($keyOrCallback)) { + $total += $keyOrCallback($row); + } else { + if (isset($row[$keyOrCallback]) && is_numeric($row[$keyOrCallback])) { + $total += $row[$keyOrCallback]; + } + } } + return $total; + } + + + /** + * Filter rows where "column" matches one of the given values. + * + * @param array $array The array to filter. + * @param string $key The key in each sub-array to compare. + * @param array $values The values to search for. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). + * @return array The filtered array. + */ + public static function whereIn(array $array, string $key, array $values, bool $strict = false): array + { + return array_filter( + $array, + fn ($row) + => isset($row[$key]) && in_array($row[$key], $values, $strict), + ); + } + - // We'll build an array of just the column's values: - // If the row is missing this column, we default to null - $temp = array_map(fn ($row) => $row[$column] ?? null, $array); + /** + * Filter rows where "column" does NOT match one of the given values. + * + * @param array $array The array to filter. + * @param string $key The key in each sub-array to compare. + * @param array $values The values to search for. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). + * @return array The filtered array. + */ + public static function whereNotIn(array $array, string $key, array $values, bool $strict = false): array + { + return array_filter( + $array, + fn ($row) + => !isset($row[$key]) || !in_array($row[$key], $values, $strict), + ); + } - // If callback is null, filter out falsey values - $filtered = array_filter( - $temp, - $callback ?? static fn ($val) => (bool) $val, - ARRAY_FILTER_USE_BOTH + + /** + * Filter rows where a column is null. + * + * This method takes a 2D array and a key as parameters. It returns a new array + * containing only the rows where the specified key exists and its value is null. + * + * @param array $array The array to filter. + * @param string $key The key in each sub-array to check for null value. + * @return array The filtered array with rows where the specified key is null. + */ + public static function whereNull(array $array, string $key): array + { + return array_filter( + $array, + fn ($row) + => !empty($row) && array_key_exists($key, $row) && $row[$key] === null, ); + } + + + /** + * Filter rows where a column is not null. + * + * This method takes a 2D array and a key as parameters. It returns a new array + * containing only the rows where the specified key exists and its value is not null. + * + * @param array $array The array to filter. + * @param string $key The key in each sub-array to check for non-null value. + * @return array The filtered array with rows where the specified key is not null. + */ + public static function whereNotNull(array $array, string $key): array + { + return array_filter($array, fn ($row) => isset($row[$key])); + } + + + /** + * Group a 2D array by a given column or callback. + * + * This method takes a 2D array and a key or a callback as parameters. + * It returns a new array containing the grouped data. + * + * If the grouping key is a string, it is used as a key in each sub-array to group by. + * If the grouping key is a callable, it is called with each sub-array and its key as arguments, + * and the return value is used as the grouping key. + * + * If the `$preserveKeys` parameter is true, the original key from the array is preserved + * in the grouped array. Otherwise, the grouped array values are indexed numerically. + * + * @param array $array The array to group. + * @param string|callable $groupBy The key or callback to group by. + * @param bool $preserveKeys Whether to preserve the original key in the grouped array. + * @return array The grouped array. + */ + public static function groupBy(array $array, string|callable $groupBy, bool $preserveKeys = false): array + { + $results = []; + foreach ($array as $key => $row) { + $gKey = null; + if (is_callable($groupBy)) { + $gKey = $groupBy($row, $key); + } elseif (isset($row[$groupBy])) { + $gKey = $row[$groupBy]; + } else { + $gKey = '_undefined'; + } - // Rebuild final results by picking only the rows that survived the filter - $return = []; - foreach ($filtered as $key => $_value) { - $return[$key] = $array[$key]; + if ($preserveKeys) { + $results[$gKey][$key] = $row; + } else { + $results[$gKey][] = $row; + } } + return $results; + } + - return $return; + /** + * Sort a 2D array by a specified column or using a callback function. + * + * This method sorts an array based on a given column name or a custom callback. + * The sorting can be performed in ascending or descending order, and it allows + * specifying sorting options. + * + * @param array $array The array to sort. + * @param string|callable $by The column key to sort by, or a callable function that returns the value to sort by. + * @param bool $desc Whether to sort in descending order. Defaults to false (ascending order). + * @param int $options The sorting options. Defaults to SORT_REGULAR. + * @return array The sorted array. + */ + public static function sortBy( + array $array, + string|callable $by, + bool $desc = false, + int $options = \SORT_REGULAR, + ): array { + uasort($array, function ($a, $b) use ($by, $desc, $options) { + $valA = is_callable($by) ? $by($a) : ($a[$by] ?? null); + $valB = is_callable($by) ? $by($b) : ($b[$by] ?? null); + + if ($valA === $valB) { + return 0; + } + $comparison = ($valA < $valB) ? -1 : 1; + return $desc ? -$comparison : $comparison; + }); + return $array; + } + + + /** + * Sort a 2D array by a specified column or using a callback function, in descending order. + * + * This is a convenience method for calling `sortBy` with the third argument set to true. + * + * @param array $array The array to sort. + * @param string|callable $by The column key to sort by, or a callable function that returns the value to sort by. + * @param int $options The sorting options. Defaults to SORT_REGULAR. + * @return array The sorted array. + */ + public static function sortByDesc(array $array, string|callable $by, int $options = \SORT_REGULAR): array + { + return static::sortBy($array, $by, true, $options); } } diff --git a/src/Array/ArraySingle.php b/src/Array/ArraySingle.php index 7c21cc3..9e7d4fd 100644 --- a/src/Array/ArraySingle.php +++ b/src/Array/ArraySingle.php @@ -4,45 +4,51 @@ namespace Infocyph\ArrayKit\Array; -/** - * Class ArraySingle - * - * Provides operations for one-dimensional arrays, including - * basic checks, slicing, and other common manipulations. - */ class ArraySingle { /** - * Determine if the given key exists in the provided array. + * Check if a given key exists in a single-dimensional array. * - * @param array $array The array to inspect - * @param int|string $key The key to check - * @return bool True if the key exists + * This method determines whether the specified key is present + * in the array, either by checking if it is set or if it exists + * as a key in the array. + * + * @param array $array The array to search in. + * @param int|string $key The key to check for existence. + * @return bool True if the key exists in the array, false otherwise. */ public static function exists(array $array, int|string $key): bool { - // Note: isset() returns false for null values, - // but array_key_exists() accounts for them. return isset($array[$key]) || array_key_exists($key, $array); } + /** - * Get a subset of the items from the given array by specifying keys. + * Select only certain keys from a single-dimensional array. + * + * This method is the single-dimensional equivalent of ArrayMulti::only. * - * @param array $array The array to filter - * @param array|string $keys A single key or multiple keys to keep - * @return array Array containing only the specified keys + * @param array $array The array to select from. + * @param array|string $keys The keys to select. + * @return array A new array with the selected keys. */ public static function only(array $array, array|string $keys): array { return array_intersect_key($array, array_flip((array) $keys)); } + /** - * Separate an array into two arrays: one with keys, the other with values. + * Split an array into separate arrays of keys and values. + * + * Useful for destructuring an array into separate key and value arrays. * - * @param array $array The array to separate - * @return array An associative array with 'keys' and 'values' + * @param array $array The array to split. + * @return array A new array containing two child arrays: 'keys' and 'values'. + * @example + * $data = ['a' => 1, 'b' => 2, 'c' => 3]; + * $keysAndValues = ArraySingle::separate($data); + * // $keysAndValues === ['keys' => ['a', 'b', 'c'], 'values' => [1, 2, 3]]; */ public static function separate(array $array): array { @@ -52,37 +58,48 @@ public static function separate(array $array): array ]; } + /** - * Determine if an array is a sequential/list array (0-based, consecutive integer keys). + * Determine if an array is a strict list (i.e., has no string keys). * - * @param array $array The array to check - * @return bool True if the array is sequential + * A strict list is an array where all keys are integers and are in sequence + * from 0 to n-1, where n is the length of the array. + * + * @param array $array The array to test. + * @return bool True if the array is a strict list, false otherwise. */ public static function isList(array $array): bool { - // Checks if first key is 0 and keys are consecutive range from 0...count-1 return static::exists($array, 0) && array_keys($array) === range(0, count($array) - 1); } + /** - * Determine if an array is associative (i.e., not a strict list). + * Determine if an array is an associative array (i.e., has string keys). + * + * An associative array is an array where at least one key is a string. * - * @param array $array The array to check - * @return bool True if the array is associative + * @param array $array The array to test. + * @return bool True if the array is an associative array, false otherwise. */ public static function isAssoc(array $array): bool { return array_keys($array) !== range(0, count($array) - 1); } + /** - * Push an item onto the beginning of an array, optionally with a specific key. + * Prepend a value to the beginning of an array. + * + * If the second parameter is null, the value is prepended as the first element + * in the array. If the second parameter is a key, the value is prepended with + * that key. * - * @param array $array The original array - * @param mixed $value The value to prepend - * @param mixed|null $key If specified, will be used as the key - * @return array The modified array + * @param array $array The array to prepend to. + * @param mixed $value The value to prepend. + * @param mixed $key The key to prepend with. If null, the value is prepended as the first element. + * @return array The modified array. */ public static function prepend(array $array, mixed $value, mixed $key = null): array { @@ -94,123 +111,141 @@ public static function prepend(array $array, mixed $value, mixed $key = null): a return $array; } + /** * Determine if all values in the array are positive numbers. * - * @param array $array The array to check - * @return bool True if every value is > 0 + * @param array $array The array to check. + * @return bool True if all values are positive, false otherwise. */ public static function isPositive(array $array): bool { - // min() > 0 ensures all values are greater than zero return !empty($array) && min($array) > 0; } + /** * Determine if all values in the array are negative numbers. * - * @param array $array The array to check - * @return bool True if every value is < 0 + * @param array $array The array to check. + * @return bool True if all values are negative, false otherwise. */ public static function isNegative(array $array): bool { - // max() < 0 ensures all values are less than zero return !empty($array) && max($array) < 0; } + /** - * Shuffle the array. If a seed is provided, shuffle predictably. + * Randomly shuffles the elements in the given array. + * + * If no seed is given, the internal PHP random number generator is used. + * If a seed is given, the Mersenne Twister random number generator is + * seeded with the given value, used to shuffle the array, and then reset + * to the current internal PHP random number generator seed. * - * @param array $array The array to shuffle - * @param int|null $seed Optional seed for randomization - * @return array The shuffled array + * @param array $array The array to shuffle. + * @param int|null $seed Optional seed for the Mersenne Twister. + * @return array The shuffled array. */ public static function shuffle(array $array, ?int $seed = null): array { if ($seed === null) { - shuffle($array); + \shuffle($array); } else { - mt_srand($seed); - shuffle($array); - mt_srand(); + \mt_srand($seed); + \shuffle($array); + \mt_srand(); } return $array; } + /** - * Determine if all values in the array are integers. + * Check if all values in the array are integers. * - * @param array $array The array to check - * @return bool True if every value is an integer + * @param array $array The array to check. + * @return bool True if all values are integers, false otherwise. */ public static function isInt(array $array): bool { return $array === static::where($array, 'is_int'); } + /** - * Return all non-empty values (non-null, non-empty strings, non-false) from the array. + * Get only the non-empty values from the array. + * + * A value is considered non-empty if it is not an empty string. * - * @param array $array The array to filter - * @return array Filtered array of non-empty values + * @param array $array The array to check. + * @return array The non-empty values. */ public static function nonEmpty(array $array): array { return array_values(static::where($array, 'strlen')); } + /** - * Calculate the average (mean) of numeric values in the array. + * Calculate the average of an array of numbers. * - * @param array $array Array of numeric values - * @return float|int The average value + * @param array $array The array of numbers to average. + * @return float|int The average of the numbers in the array. If the array is empty, 0 is returned. */ public static function avg(array $array): float|int { + if (empty($array)) { + return 0; + } return array_sum($array) / count($array); } + /** - * Check if the array contains only unique values. + * Determine if all values in the array are unique. * - * @param array $array The array to check - * @return bool True if all values are unique + * @param array $array The array to check. + * @return bool True if all values are unique, false otherwise. */ public static function isUnique(array $array): bool { - // Compare the array count to the count of flipped keys. return count($array) === count(array_flip($array)); } + /** * Get only the positive numeric values from the array. * - * @param array $array The array to filter - * @return array Array containing only positive values + * @param array $array The array to check. + * @return array The positive numeric values. */ public static function positive(array $array): array { return static::where($array, static fn ($value) => is_numeric($value) && $value > 0); } + /** * Get only the negative numeric values from the array. * - * @param array $array The array to filter - * @return array Array containing only negative values + * @param array $array The array to check. + * @return array The negative numeric values. */ public static function negative(array $array): array { return static::where($array, static fn ($value) => is_numeric($value) && $value < 0); } + /** - * Return every n-th element in the array, with an optional offset. + * Get every n-th element from the array * - * @param array $array The array to slice - * @param int $step The step rate - * @param int $offset The offset index to start picking from - * @return array The n-th elements + * @param array $array The array to slice. + * @param int $step The "step" value (i.e. the interval between selected elements). + * @param int $offset The offset from which to begin selecting elements. + * + * @return array The sliced array. */ public static function nth(array $array, int $step, int $offset = 0): array { @@ -227,11 +262,14 @@ public static function nth(array $array, int $step, int $offset = 0): array return $results; } + /** - * Retrieve an array of duplicate values (values that appear more than once). + * Retrieve duplicate values from an array. + * + * This method returns an array of values that occur more than once in the input array. * - * @param array $array The array to inspect - * @return array An indexed array of duplicate values + * @param array $array The array to search for duplicates. + * @return array An array of duplicate values. */ public static function duplicates(array $array): array { @@ -244,13 +282,15 @@ public static function duplicates(array $array): array return $duplicates; } + /** - * "Paginate" the array by slicing it into a smaller array segment. + * "Paginate" the array by slicing it into a smaller segment. + * + * @param array $array The array to paginate. + * @param int $page The page number to retrieve (1-indexed). + * @param int $perPage The number of items per page. * - * @param array $array The array to paginate - * @param int $page The current page (1-based) - * @param int $perPage Number of items per page - * @return array The slice of the array for the specified page + * @return array The paginated slice of the array. */ public static function paginate(array $array, int $page, int $perPage): array { @@ -262,13 +302,18 @@ public static function paginate(array $array, int $page, int $perPage): array ); } + /** - * Generate an array by using one array for keys and another for values. - * If one array is shorter, only matches pairs up to that length. + * Combine two arrays into one array with corresponding key-value pairs. * - * @param array $keys The array of keys - * @param array $values The array of values - * @return array The combined array; empty if both arrays are empty + * The function takes two arrays, one of keys and one of values, and combines them + * into a single array. If the two arrays are not of equal length, the function + * will truncate the longer array to match the length of the shorter array. + * + * @param array $keys The array of keys. + * @param array $values The array of values. + * + * @return array The combined array. */ public static function combine(array $keys, array $values): array { @@ -284,30 +329,42 @@ public static function combine(array $keys, array $values): array return array_combine($keys, $values) ?: []; } + /** - * Filter the array with a callback. If no callback is provided, - * returns only the non-null, non-false values. + * Filter the array using a callback function. * - * @param array $array The array to filter - * @param callable|null $callback The callback to apply; signature: fn($value, $key): bool - * @return array Filtered array (preserves keys) + * If the callback is omitted, the function will return all elements in the + * array that are truthy. + * + * @param array $array The array to search. + * @param callable|null $callback The callback function to use for filtering. + * This function should take two arguments, the value and the key of each + * element in the array. The function should return true for elements that + * should be kept, and false for elements that should be discarded. + * + * @return array The filtered array. */ public static function where(array $array, ?callable $callback = null): array { - $flag = ($callback !== null) ? ARRAY_FILTER_USE_BOTH : 0; + $flag = ($callback !== null) ? \ARRAY_FILTER_USE_BOTH : 0; return array_filter($array, $callback ?? fn ($val) => (bool) $val, $flag); } + /** - * Search the array for the first item that matches a given condition or value. + * Search the array for a given value and return its key if found. * - * Usage: - * - If $needle is a callback, we'll check each $element until $callback($element, $key) === true. - * - Otherwise, if $needle is not a callable, we do a direct "value === $needle" check. + * If the value is a callable, it will be called for each element in the array, + * and if the callback returns true, the key will be returned. If the value is + * not a callable, the function will search for the value in the array using + * strict comparison. If the value is found, its key will be returned. If the + * value is not found, null will be returned. * - * @param array $array The array to search - * @param mixed|callable $needle The value to find or a callback - * @return int|string|null The key if found, or null if not found + * @param array $array The array to search. + * @param mixed $needle The value to search for, or a callable to use for + * searching. + * + * @return int|string|null The key of the value if found, or null if not found. */ public static function search(array $array, mixed $needle): int|string|null { @@ -319,9 +376,353 @@ public static function search(array $array, mixed $needle): int|string|null } return null; } - - // Direct search for a value $foundKey = array_search($needle, $array, true); return $foundKey === false ? null : $foundKey; } + + + /** + * Break an array into smaller chunks of a specified size. + * + * This function splits the input array into multiple smaller arrays, each + * containing up to the specified number of elements. If the specified size + * is less than or equal to zero, the entire array is returned as a single chunk. + * + * @param array $array The array to be chunked. + * @param int $size The size of each chunk. + * @param bool $preserveKeys Whether to preserve the keys in the chunks. + * + * @return array An array of arrays, each representing a chunk of the original array. + */ + public static function chunk(array $array, int $size, bool $preserveKeys = false): array + { + if ($size <= 0) { + return [$array]; + } + return array_chunk($array, $size, $preserveKeys); + } + + + /** + * Apply a callback to each item in the array, optionally preserving keys. + * + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return the value to be used + * in the resulting array. + * + * @param array $array The array to be mapped over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The array with each element transformed by the callback. + */ + public static function map(array $array, callable $callback): array + { + $results = []; + foreach ($array as $key => $value) { + $results[$key] = $callback($value, $key); + } + return $results; + } + + + /** + * Execute a callback on each item in the array, returning the original array. + * + * The callback function receives two arguments: the value of the current + * element and its key. The callback should return a value that can be + * evaluated to boolean. If the callback returns false, the iteration is + * broken. Otherwise, the iteration continues. + * + * @param array $array The array to be iterated over. + * @param callable $callback The callback function to apply to each element. + * + * @return array The original array. + */ + public static function each(array $array, callable $callback): array + { + foreach ($array as $key => $value) { + if ($callback($value, $key) === false) { + break; + } + } + return $array; + } + + + /** + * Reduce an array to a single value using a callback function. + * + * The callback function should accept three arguments: the accumulator, + * the current array value, and the current array key. It should return + * the updated accumulator value. + * + * @param array $array The array to reduce. + * @param callable $callback The callback function to apply to each element. + * @param mixed $initial The initial value of the accumulator. + * @return mixed The reduced value. + */ + public static function reduce(array $array, callable $callback, mixed $initial = null): mixed + { + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $callback($accumulator, $value, $key); + } + return $accumulator; + } + + + /** + * Determine if at least one element in the array passes the given truth test. + * + * @param array $array The array to search. + * @param callable $callback The callback to apply to each element. + * @return bool Whether at least one element passed the truth test. + */ + public static function some(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + return true; + } + } + return false; + } + + + /** + * Determine if all elements in the array pass the given truth test. + * + * @param array $array The array to search. + * @param callable $callback The callback to apply to each element. + * @return bool Whether all elements passed the truth test. + */ + public static function every(array $array, callable $callback): bool + { + foreach ($array as $key => $value) { + if (!$callback($value, $key)) { + return false; + } + } + return true; + } + + + /** + * Determine if the array contains a given value or if a callback function + * returns true for at least one element. + * + * If the second argument is a callable, it is used as a callback function + * that receives the value and key of each element in the array. If the + * callback returns true, the function returns true. + * + * If the second argument is not a callable, it is used as the value to + * search for in the array. The optional third argument determines whether + * to use strict comparison (===) or loose comparison (==). + * + * @param array $array The array to search. + * @param mixed $valueOrCallback The value to search for, or a callable to apply to each element. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). + * @return bool Whether the array contains the given value or whether the callback returned true for at least one element. + */ + public static function contains(array $array, mixed $valueOrCallback, bool $strict = false): bool + { + if (is_callable($valueOrCallback)) { + return static::some($array, $valueOrCallback); + } + return in_array($valueOrCallback, $array, $strict); + } + + + /** + * Return the sum of all the elements in the array. + * + * If a callback is provided, it will be executed for each element in the + * array and the return value will be added to the total. + * + * @param array $array The array to sum. + * @param callable|null $callback The callback to execute for each element. + * @return float|int The sum of all the elements in the array. + */ + public static function sum(array $array, ?callable $callback = null): float|int + { + if ($callback === null) { + return array_sum($array); + } + + $total = 0; + foreach ($array as $value) { + $total += $callback($value); + } + return $total; + } + + + /** + * Return an array with all duplicate values removed. + * + * The second parameter, $strict, determines whether to use strict comparison (===) or loose comparison (==) when + * checking for duplicate values. If not provided, it defaults to false, which means loose comparison will be used. + * + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to remove duplicates from. + * @param bool $strict Whether to use strict comparison (===) or loose comparison (==). Defaults to false. + * @return array The array with all duplicate values removed. + */ + public static function unique(array $array, bool $strict = false): array + { + if (!$strict) { + return array_values(array_unique($array)); + } + // Manual strict approach: + $checked = []; + $result = []; + foreach ($array as $item) { + if (!in_array($item, $checked, true)) { + $checked[] = $item; + $result[] = $item; + } + } + return $result; + } + + + /** + * Return an array with all values that do not pass the given callback. + * + * The method takes an array and an optional callback as parameters. + * If the callback is not provided, it defaults to `true`, which means the method will return an array with all + * values that are not equal to `true`. + * If the callback is a callable, the method will use it to filter the array. If the callback returns `false` for + * a value, that value will be rejected. + * If the callback is not a callable, the method will use it as the value to compare against. If the value is equal + * to the callback, it will be rejected. + * + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to filter. + * @param mixed $callback The callback to use for filtering, or the value to compare against. Defaults to `true`. + * @return array The filtered array. + */ + public static function reject(array $array, mixed $callback = true): array + { + return BaseArrayHelper::doReject($array, $callback); + } + + + /** + * Return a slice of the array, starting from the given offset and with the given length. + * + * The method takes three parameters: the array to slice, the offset from which to start the slice, + * and the length of the slice. If the length is not provided, the method will return all elements + * starting from the given offset. + * + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to slice. + * @param int $offset The offset from which to start the slice. + * @param int|null $length The length of the slice. If not provided, the method will return all elements + * starting from the given offset. + * @return array The sliced array. + */ + public static function slice(array $array, int $offset, ?int $length = null): array + { + return array_slice($array, $offset, $length, true); + } + + + /** + * Skip the first $count items of the array and return the remainder. + * + * This method is an alias for `slice($array, $count)`. + * + * @param array $array The array to skip. + * @param int $count The number of items to skip. + * @return array The skipped array. + */ + public static function skip(array $array, int $count): array + { + return static::slice($array, $count); + } + + + /** + * Skip items while the callback returns true; once false, keep the remainder. + * + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * As long as the callback returns true, the item is skipped. + * The first item for which the callback returns false is kept, + * and all subsequent items are also kept. + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to skip. + * @param callable $callback The callback to use for skipping. + * @return array The skipped array. + */ + public static function skipWhile(array $array, callable $callback): array + { + $result = []; + $skipping = true; + + foreach ($array as $key => $value) { + if ($skipping && !$callback($value, $key)) { + $skipping = false; + } + if (!$skipping) { + $result[$key] = $value; + } + } + return $result; + } + + + /** + * Skip items until the callback returns true, then keep the remainder. + * + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * As long as the callback returns false, the item is skipped. + * The first item for which the callback returns true is kept, + * and all subsequent items are also kept. + * The method returns an array with the same type of indices as the input array. + * + * @param array $array The array to skip. + * @param callable $callback The callback to use for skipping. + * @return array The skipped array. + */ + public static function skipUntil(array $array, callable $callback): array + { + return static::skipWhile($array, fn ($value, $key) => !$callback($value, $key)); + } + + + /** + * Partition the array into two arrays [passed, failed] based on a callback. + * + * The method takes an array and a callback as parameters. + * It iterates over the array, applying the callback to each item. + * If the callback returns true, the item is added to the "passed" array. + * If the callback returns false, the item is added to the "failed" array. + * The method returns an array with two elements, the first being the "passed" array, + * and the second being the "failed" array. + * + * @param array $array The array to partition. + * @param callable $callback The callback to use for partitioning. + * @return array An array with two elements, the first being the "passed" array, and the second being the "failed" array. + */ + public static function partition(array $array, callable $callback): array + { + $passed = []; + $failed = []; + + foreach ($array as $key => $value) { + if ($callback($value, $key)) { + $passed[$key] = $value; + } else { + $failed[$key] = $value; + } + } + return [$passed, $failed]; + } } diff --git a/src/Array/BaseArrayHelper.php b/src/Array/BaseArrayHelper.php index a648aaa..25714b9 100644 --- a/src/Array/BaseArrayHelper.php +++ b/src/Array/BaseArrayHelper.php @@ -4,52 +4,71 @@ namespace Infocyph\ArrayKit\Array; -/** - * Class BaseArrayHelper - * - * Provides basic utility methods for array operations, - * including checks for multidimensionality and wrapping - * single values into arrays. - */ +use ArrayAccess; +use InvalidArgumentException; + class BaseArrayHelper { /** - * Determine if a variable is truly a multidimensional array. + * Check if an array is multi-dimensional. * - * We compare count($array) with count($array, COUNT_RECURSIVE): - * if they differ, there's at least one nested array. + * This method is a shortcut for checking if an array is multi-dimensional. + * It checks if the array is an array and if the count of the array is not + * equal to the count of the array with the COUNT_RECURSIVE flag. * - * @param mixed $array Value to check - * @return bool True if multidimensional, otherwise false + * @param mixed $array The array to check. + * @return bool True if the array is multi-dimensional, false otherwise. */ public static function isMultiDimensional(mixed $array): bool { - return is_array($array) && count($array) !== count($array, COUNT_RECURSIVE); + return is_array($array) + && count($array) !== count($array, COUNT_RECURSIVE); } + /** - * Wrap a value in an array if it isn't already an array (and not empty). + * Wrap a value in an array if it's not already an array; otherwise return the array as is. * - * If the given value is empty, return an empty array. + * If the value is empty, an empty array is returned. * - * @param mixed $value The value to wrap - * @return array + * @param mixed $value The value to wrap. + * @return array The wrapped value. */ public static function wrap(mixed $value): array { if (empty($value)) { return []; } - return is_array($value) ? $value : [$value]; } + + /** + * Unwrap a value from an array if it contains exactly one element. + * + * This method checks if the given value is an array. If it is not, + * the value is returned as is. If the value is an array with exactly + * one element, that element is returned. Otherwise, the array itself + * is returned. + * + * @param mixed $value The value to potentially unwrap. + * @return mixed The unwrapped value or the original array. + */ + public static function unWrap(mixed $value): mixed + { + if (!is_array($value)) { + return $value; + } + return (count($value) === 1) ? reset($value) : $value; + } + + /** - * Check if at least one item in the array passes the given truth test. + * Determine if at least one element in the array passes the given truth test. * - * @param array $array The array to inspect - * @param callable $callback A callback with signature: fn($value, $key): bool - * @return bool True if the callback returns true for any item + * @param array $array The array to search. + * @param callable $callback The callback to use for searching. + * @return bool Whether at least one element passed the truth test. */ public static function haveAny(array $array, callable $callback): bool { @@ -61,12 +80,13 @@ public static function haveAny(array $array, callable $callback): bool return false; } + /** - * Check if all items in the array pass the given truth test. + * Determine if all elements in the array pass the given truth test. * - * @param array $array The array to inspect - * @param callable $callback A callback with signature: fn($value, $key): bool - * @return bool True if the callback returns true for every item + * @param array $array The array to search. + * @param callable $callback The callback to use for searching. + * @return bool Whether all elements passed the truth test. */ public static function isAll(array $array, callable $callback): bool { @@ -78,12 +98,14 @@ public static function isAll(array $array, callable $callback): bool return true; } + /** - * Find the first key in the array for which the callback returns true. + * Search the array for a given value and return its key if found. + * + * @param array $array The array to search. + * @param callable $callback The callback to use for searching. * - * @param array $array The array to search - * @param callable $callback Callback with signature: fn($value, $key): bool - * @return int|string|null The key if found, or null if no match + * @return int|string|null The key of the value if found, or null if not found. */ public static function findKey(array $array, callable $callback): int|string|null { @@ -94,4 +116,274 @@ public static function findKey(array $array, callable $callback): int|string|nul } return null; } + + + /** + * Check if the given value is an array or an instance of ArrayAccess. + * + * @param mixed $value The value to check. + * @return bool True if the value is accessible, false otherwise. + */ + public static function accessible(mixed $value): bool + { + return is_array($value) || $value instanceof ArrayAccess; + } + + + /** + * Check if all the given keys exist in the array. + * + * @param array $array The array to search. + * @param int|string|array $keys The key(s) to check for existence. + * + * @return bool True if all the given keys exist in the array, false otherwise. + */ + public static function has(array $array, int|string|array $keys): bool + { + $keys = (array) $keys; + if (empty($keys)) { + return false; + } + + foreach ($keys as $key) { + if (!array_key_exists($key, $array)) { + return false; + } + } + return true; + } + + + /** + * Check if at least one of the given keys exists in the array. + * + * This function accepts a single key or an array of keys and + * determines whether at least one of them exists within the + * provided array. If any key is found, the function returns + * true. Otherwise, it returns false. + * + * @param array $array The array to search. + * @param int|string|array $keys The key(s) to check for existence. + * @return bool True if at least one key exists in the array, false otherwise. + */ + public static function hasAny(array $array, int|string|array $keys): bool + { + $keys = (array) $keys; + if (empty($keys)) { + return false; + } + + foreach ($keys as $key) { + if (array_key_exists($key, $array)) { + return true; + } + } + return false; + } + + + /** + * Generate an array containing a sequence of numbers. + * + * This function creates an array of numbers starting from $start up to $end, + * incrementing by $step. If $step is zero, an empty array is returned. + * + * @param int $start The starting number of the sequence. + * @param int $end The ending number of the sequence. + * @param int $step The increment between each number in the sequence. Defaults to 1. + * @return array An array containing the sequence of numbers. + */ + public static function range(int $start, int $end, int $step = 1): array + { + if ($step === 0) { + // We could throw an exception, or return empty: + return []; + } + return range($start, $end, $step); + } + + + /** + * Create an array of the specified length and fill it with the results of the + * given callback function. If the callback is not provided, the array will be + * filled with the numbers 1 through $number. + * + * Example: + * ArrayKit::times(3, function ($i) { + * return "Row #{$i}"; + * }); + * // Output: ["Row #1", "Row #2", "Row #3"] + * + * @param int $number The length of the array. + * @param callable|null $callback The callback function to use. + * @return array The filled array. + */ + public static function times(int $number, ?callable $callback = null): array + { + $results = []; + if ($number < 1) { + return $results; + } + + for ($i = 1; $i <= $number; $i++) { + $results[] = $callback ? $callback($i) : $i; + } + + return $results; + } + + + /** + * Check if at least one element in the array passes a given truth test. + * + * This function is an alias for haveAny, which is a more descriptive name. + * It is provided for syntactic sugar, as it is very common to want to + * check if at least one item in an array matches a given criteria. + * + * @param array $array The array to check. + * @param callable $callback The callback to apply to each element. + * @return bool True if at least one element passes the test, false otherwise. + */ + public static function any(array $array, callable $callback): bool + { + return static::haveAny($array, $callback); + } + + + /** + * Check if all elements in the array pass the given truth test. + * + * This function applies a callback to each element of the array. + * If the callback returns true for all elements, the function returns true. + * Otherwise, it returns false. + * + * @param array $array The array to be evaluated. + * @param callable $callback The callback function to apply to each element. + * @return bool True if all elements pass the truth test, false otherwise. + */ + public static function all(array $array, callable $callback): bool + { + return static::isAll($array, $callback); + } + + /* ------------------------------------------------------------------------ + | Additional "Sugar" Methods (Point 1) + ---------------------------------------------------------------------- */ + + + /** + * Pass the array to the given callback and return it. + * + * Useful for tapping into a fluent method chain for debugging. + * + * @param array $array The array to be tapped. + * @param callable $callback The callback to apply to the array. + * @return array The original array. + */ + public static function tap(array $array, callable $callback): array + { + $callback($array); + return $array; + } + + + /** + * Remove one or multiple array items from an array. + * + * This function takes an array and a key or array of keys as parameters. + * It then iterates over the given keys, and unsets the corresponding + * items from the array. + * + * @param array $array The array from which to remove items. + * @param int|string|array $keys The key or array of keys to be removed. + */ + public static function forget(array &$array, int|string|array $keys): void + { + foreach ((array) $keys as $key) { + unset($array[$key]); + } + } + + + /** + * Retrieve one or multiple random items from an array. + * + * By default, this function returns a single item from the array. + * If you pass a number as the second argument, it will return that + * number of items. If you set the third argument to `true`, the + * keys from the original array are preserved in the returned array. + * + * @param array $array The array from which to retrieve random items. + * @param int|null $number The number of items to retrieve. If null, a single item is returned. + * @param bool $preserveKeys Whether to preserve the keys from the original array. + * + * @return mixed The retrieved item(s) from the array. + * + * @throws InvalidArgumentException If the user requested more items than the array contains. + */ + public static function random(array $array, int $number = null, bool $preserveKeys = false): mixed + { + $count = count($array); + + // If array is empty or user requested <=0 items, handle edge-case: + if ($count === 0 || ($number !== null && $number <= 0)) { + return ($number === null) ? null : []; + } + + // If we only want one item: + if ($number === null) { + $randKey = array_rand($array); + return $array[$randKey]; + } + + if ($number > $count) { + throw new InvalidArgumentException( + "You requested $number items, but array only has $count." + ); + } + + // For multiple items: + $keys = array_rand($array, $number); + if (!is_array($keys)) { + // array_rand returns a single value when $number=1 + $keys = [$keys]; + } + + $results = []; + foreach ($keys as $key) { + if ($preserveKeys) { + $results[$key] = $array[$key]; + } else { + $results[] = $array[$key]; + } + } + + return $results; + } + + + /** + * Filter an array by rejecting elements based on a callback function or value. + * + * This function takes an array and a callback or value as parameters. + * If the callback is callable, it applies the callback to each element of the array. + * Elements for which the callback returns false are kept. + * If the callback is a value, elements equal to this value are rejected. + * The function returns an array with the same type of indices as the input array. + * + * @param array $array The array to be filtered. + * @param mixed $callback The callback function or value for filtering. + * @return array The array with elements rejected based on the callback or value. + */ + public static function doReject(array $array, mixed $callback): array + { + if (is_callable($callback)) { + return array_filter( + $array, + fn ($val, $key) => !$callback($val, $key), + ARRAY_FILTER_USE_BOTH + ); + } + return array_filter($array, fn ($val) => $val != $callback); + } } diff --git a/src/Array/DotNotation.php b/src/Array/DotNotation.php index 5e28160..4f91d77 100644 --- a/src/Array/DotNotation.php +++ b/src/Array/DotNotation.php @@ -4,20 +4,17 @@ namespace Infocyph\ArrayKit\Array; -/** - * Class DotNotation - * - * Provides utilities for working with "dot" notation on associative arrays, - * including flattening, expanding, and accessing nested data. - */ +use InvalidArgumentException; + class DotNotation { /** - * Flatten an associative array into a single level using dot notation for nested keys. + * Flattens a multidimensional array to a single level, using dot notation to + * represent nested keys. * - * @param array $array The array to flatten - * @param string $prepend Optional prefix to prepend to flattened keys - * @return array Flattened array + * @param array $array The multidimensional array to flatten. + * @param string $prepend A string to prepend to the keys of the flattened array. + * @return array A flattened array with all nested arrays collapsed to the same level. */ public static function flatten(array $array, string $prepend = ''): array { @@ -37,11 +34,14 @@ public static function flatten(array $array, string $prepend = ''): array return $results; } + /** - * Expand a "dot" notation array into a full multi-dimensional array. + * Expands a flattened array (created by flatten) back into a nested structure. * - * @param array $array The flattened array in dot notation - * @return array Expanded array + * @param array $array A flattened array, where each key is a string with dot + * notation representing the nested keys. + * @return array A nested array with the same values as the input but with the + * nested structure restored. */ public static function expand(array $array): array { @@ -49,16 +49,17 @@ public static function expand(array $array): array foreach ($array as $key => $value) { static::set($results, $key, $value); } - return $results; } /** - * Determine if an item or items exist in an array using dot notation. + * Determine if the given key or keys exist in the array. + * + * This method is the dot notation aware version of ArraySingle::has. * - * @param array $array The array to inspect - * @param array|string $keys One or multiple keys in dot notation - * @return bool True if all specified keys exist + * @param array $array The array to search. + * @param array|string $keys The key(s) to check for existence. + * @return bool True if all the given keys exist in the array, false otherwise. */ public static function has(array $array, array|string $keys): bool { @@ -66,32 +67,31 @@ public static function has(array $array, array|string $keys): bool return false; } - // If the key is a string and exists at top level, return immediately. + // If single string key and found top-level: if (is_string($keys) && ArraySingle::exists($array, $keys)) { return true; } $keys = (array) $keys; foreach ($keys as $key) { - // If this key is found at the top level, continue. if (ArraySingle::exists($array, $key)) { continue; } - // Otherwise check dot notation segments. - if (!static::segment($array, $key, false)) { + // Fall back to a simple segment check (no wildcard) + if (!static::segmentExact($array, $key, false)) { return false; } } - return true; } + /** - * Determine if any of the provided keys exist in an array using dot notation. + * Check if *any* of the given keys exist (no wildcard). * - * @param array $array The array to inspect - * @param array|string $keys One or multiple keys in dot notation - * @return bool True if at least one specified key exists + * @param array $array The array to search. + * @param array|string $keys The key(s) to check for existence. + * @return bool True if at least one key exists */ public static function hasAny(array $array, array|string $keys): bool { @@ -105,172 +105,290 @@ public static function hasAny(array $array, array|string $keys): bool return true; } } - return false; } + /** - * Get an item or items from an array using dot notation. + * Get one or multiple items from the array using dot notation. * - * @param array $array The array to retrieve from - * @param array|int|string|null $keys The key(s) in dot notation or integer - * @param mixed|null $default Default value if the key doesn't exist - * @return mixed The retrieved value(s) or default + * The following cases are handled: + * - If no key is provided, the entire array is returned. + * - If an array of keys is provided, all values are returned in an array. + * - If a single key is provided, the value is returned directly. + * + * @param array $array The array to retrieve items from. + * @param array|int|string|null $keys The key(s) to retrieve. + * @param mixed $default The default value to return if the key is not found. + * @return mixed The retrieved value(s). */ public static function get(array $array, array|int|string $keys = null, mixed $default = null): mixed { - if (empty($array)) { - return $default; - } - - // If no key specified, return the entire array. + // If no key, return entire array if ($keys === null) { return $array; } - // If multiple keys requested, gather each value. + // If multiple keys requested, gather each value: if (is_array($keys)) { $results = []; - foreach ($keys as $key) { - $results[$key] = static::getValue($array, $key, $default); + foreach ($keys as $k) { + $results[$k] = static::getValue($array, $k, $default); } return $results; } - // Single key retrieval. + // single key return static::getValue($array, $keys, $default); } + /** - * Set an array item (or items) to a given value using dot notation. + * Set one or multiple items in the array using dot notation. * - * @param array $array The original array (passed by reference) - * @param array|string|null $keys Key(s) in dot notation - * @param mixed|null $value The value to set - * @return bool Returns true on success + * If no key is provided, the entire array is replaced with $value. + * If an array of key-value pairs is provided, each value is set. + * If a single key is provided, the value is set directly. + * + * @param array $array The array to set items in. + * @param array|string|null $keys The key(s) to set. + * @param mixed $value The value to set. + * @param bool $overwrite If true, existing values are overwritten. If false, existing values are preserved. + * @return bool True on success */ - public static function set(array &$array, array|string|null $keys = null, mixed $value = null): bool + public static function set(array &$array, array|string|null $keys = null, mixed $value = null, bool $overwrite = true): bool { - // If no key is specified, replace the entire array. + // If no key, replace entire array with $value if ($keys === null) { $array = (array) $value; return true; } - // If multiple sets are requested - $keyValueList = is_array($keys) ? $keys : [$keys => $value]; - foreach ($keyValueList as $key => $val) { - static::setValue($array, (string) $key, $val); + if (is_array($keys)) { + // multiple sets + foreach ($keys as $k => $val) { + static::setValue($array, $k, $val, $overwrite); + } + } else { + static::setValue($array, $keys, $value, $overwrite); } return true; } + /** - * Remove one or multiple array items from a given array using dot notation. + * Fill in data where missing (like set, but doesn't overwrite existing keys). * - * @param array $array The original array (passed by reference) - * @param array|string $keys The key(s) to remove + * @param array $array The array to fill in. + * @param array|string $keys The key(s) to fill in. + * @param mixed $value The value to set if missing. * @return void */ - public static function forget(array &$array, array|string $keys): void + public static function fill(array &$array, array|string $keys, mixed $value = null): void { - $original = &$array; - $keys = (array) $keys; + static::set($array, $keys, $value, false); + } - if (count($keys) === 0) { + /** + * Remove one or multiple items from an array or object using dot notation. + * + * This method supports wildcard and dot notation for nested arrays or objects. + * If a wildcard ('*') is encountered, it applies the forget operation to each + * accessible element. For objects, it unsets the specified property. + * + * @param array $target The target array or object to remove items from. + * @param array|string|int|null $keys The key(s) or path(s) to be removed. + * Supports dot notation and wildcards. + * @return void + */ + public static function forget(array &$target, array|string|int|null $keys): void + { + if ($keys === null || $keys === []) { return; } - foreach ($keys as $key) { - // If exists at top-level, simply unset. - if (ArraySingle::exists($array, $key)) { - unset($array[$key]); - continue; - } + // Convert keys to segments. + $segments = is_array($keys) ? $keys : explode('.', (string)$keys); + $segment = array_shift($segments); + + match (true) { + // Case 1: Wildcard on an accessible array. + $segment === '*' && BaseArrayHelper::accessible($target) => + count($segments) > 0 ? static::forgetEach($target, $segments) : null, + + // Case 2: Target is array-accessible (normal array). + BaseArrayHelper::accessible($target) => + count($segments) > 0 && ArraySingle::exists($target, $segment) + ? static::forget($target[$segment], $segments) + : BaseArrayHelper::forget($target, $segment), + + // Case 3: Target is an object. + is_object($target) => + count($segments) > 0 && isset($target->{$segment}) + ? static::forget($target->{$segment}, $segments) + : (isset($target->{$segment}) ? static::unsetProperty($target, $segment) : null), + + default => null, + }; + } - // Otherwise navigate the dot path. - $parts = explode('.', (string) $key); - $array = &$original; - $count = count($parts); - - foreach ($parts as $i => $part) { - if ($count - $i === 1) { - break; - } - if (isset($array[$part]) && is_array($array[$part])) { - $array = &$array[$part]; - } else { - continue 2; - } - } - unset($array[$parts[$count - 1]]); + /** + * Recursively apply the forget logic to each element in an array. + * + * This function iterates over each element of the provided array + * and applies the forget operation using the given segments. + * + * @param array $array The array whose elements will be processed. + * @param array $segments The segments to use for the forget operation. + * @return void + */ + private static function forgetEach(array &$array, array $segments): void + { + foreach ($array as &$inner) { + static::forget($inner, $segments); } } + /** - * Add an item to the array only if it does not exist using dot notation. + * Unset a property from an object. + * + * This method removes a specified property from an object by using + * PHP's unset function. The property is directly removed from the + * object if it exists. * - * @param array $array The original array (passed by reference) - * @param string|int|float $key The dot-notation key - * @param mixed $value Value to add - * @return bool Returns true if the item was added, false if it already exists + * @param object $object The object from which the property should be removed. + * @param string $property The name of the property to unset. + * @return void */ - public static function add(array &$array, string|int|float $key, mixed $value): bool + private static function unsetProperty(object &$object, string $property): void { - if (static::get($array, $key) === null) { - static::set($array, (string) $key, $value); - return true; + unset($object->{$property}); + } + + /** + * Retrieve a string value from the array using a dot-notation key. + * + * This function attempts to retrieve a value from the given array + * using the specified key. If the retrieved value is not of type + * string, an InvalidArgumentException is thrown. If the key is not + * found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return string The retrieved string value. + * @throws InvalidArgumentException If the retrieved value is not a string. + */ + public static function string(array $array, string $key, mixed $default = null): string + { + $value = static::get($array, $key, $default); + if (!is_string($value)) { + throw new InvalidArgumentException("Expected string, got " . get_debug_type($value)); } - return false; + return $value; } /** - * Get a value from the array (by dot notation), and remove it. + * Retrieve an integer value from the array using a dot-notation key. * - * @param array $array The original array (passed by reference) - * @param string $key Dot-notation key - * @param mixed $default Default if key not found - * @return mixed The found value or default + * This method tries to fetch a value from the given array with the specified key. + * If the value is not an integer, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return int The retrieved integer value. + * @throws InvalidArgumentException If the retrieved value is not an integer. */ - public static function pull(array &$array, string $key, mixed $default = null): mixed + public static function integer(array $array, string $key, mixed $default = null): int { $value = static::get($array, $key, $default); - static::forget($array, $key); + if (!is_int($value)) { + throw new InvalidArgumentException("Expected int, got " . get_debug_type($value)); + } return $value; } /** - * Append a value onto the end of an array item by dot notation key. + * Retrieve a float value from the array using a dot-notation key. * - * @param array $array The original array (passed by reference) - * @param mixed $value Value to append - * @param string|null $key Dot-notation key (if null, appends to root array) - * @return void + * This method tries to fetch a value from the given array with the specified key. + * If the value is not a float, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return float The retrieved float value. + * @throws InvalidArgumentException If the retrieved value is not a float. */ - public static function append(array &$array, mixed $value, ?string $key = null): void + public static function float(array $array, string $key, mixed $default = null): float { - if ($key !== null) { - $target = static::get($array, $key, []); - } else { - $target = $array; + $value = static::get($array, $key, $default); + if (!is_float($value)) { + throw new InvalidArgumentException("Expected float, got " . get_debug_type($value)); } + return $value; + } - $target[] = $value; - static::set($array, $key, $target); + /** + * Retrieve a boolean value from the array using a dot-notation key. + * + * This method tries to fetch a value from the given array with the specified key. + * If the value is not a boolean, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return bool The retrieved boolean value. + * @throws InvalidArgumentException If the retrieved value is not a boolean. + */ + public static function boolean(array $array, string $key, mixed $default = null): bool + { + $value = static::get($array, $key, $default); + if (!is_bool($value)) { + throw new InvalidArgumentException("Expected bool, got " . get_debug_type($value)); + } + return $value; + } + + /** + * Retrieve an array value from the array using a dot-notation key. + * + * This method tries to fetch a value from the given array with the specified key. + * If the value is not an array, an InvalidArgumentException is thrown. + * If the key is not found, the default value is returned. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to use for retrieval. + * @param mixed $default The default value to return if the key is not found. + * @return array The retrieved array value. + * @throws InvalidArgumentException If the retrieved value is not an array. + */ + public static function arrayValue(array $array, string $key, mixed $default = null): array + { + $value = static::get($array, $key, $default); + if (!is_array($value)) { + throw new InvalidArgumentException("Expected array, got " . get_debug_type($value)); + } + return $value; } /** - * Retrieve multiple keys from the array (by dot notation) at once. + * Pluck one or more values from an array. * - * Example: - * DotNotation::pluck($array, ['user.name', 'user.email']); + * This method allows you to retrieve one or more values from an array + * using dot-notation keys. * - * @param array $array The array to retrieve from - * @param array|string $keys One or multiple dot-notation keys - * @param mixed $default Default value if a key doesn't exist - * @return array An associative array of [key => value], preserving each requested key + * @param array $array The array to retrieve values from. + * @param array|string $keys The key(s) to retrieve. + * @param mixed $default The default value to return if the key is not found. + * @return array The retrieved values. */ public static function pluck(array $array, array|string $keys, mixed $default = null): array { @@ -280,75 +398,469 @@ public static function pluck(array $array, array|string $keys, mixed $default = foreach ($keys as $key) { $results[$key] = static::get($array, $key, $default); } - return $results; } /** - * Internal helper to set a nested array item using dot notation. + * Get all the given array. + * + * @param array $array + * @return array + */ + public static function all(array $array): array + { + return $array; + } + + /** + * Pass the array to the given callback and return it. + * + * Useful for tapping into a fluent method chain for debugging. + * + * @param array $array The array to be tapped. + * @param callable $callback The callback to apply to the array. + * @return array The original array. + */ + public static function tap(array $array, callable $callback): array + { + $callback($array); + return $array; + } + + /** + * Check if a given key exists in the array using dot notation. + * + * This method determines if the specified key is present + * within the provided array. It leverages the dot notation + * to access nested data structures. * - * @param array $array Reference to the main array - * @param string $key The dot-notation key - * @param mixed $value The value to set + * @param array $array The array to search. + * @param string $key The dot-notation key to check for existence. + * @return bool True if the key exists, false otherwise. + */ + public static function offsetExists(array $array, string $key): bool + { + return static::has($array, $key); + } + + /** + * Retrieves a value from the array using dot notation. + * + * This method is a part of the ArrayAccess implementation. + * + * @param array $array The array to retrieve the value from. + * @param string $key The dot-notation key to retrieve. + * @return mixed The retrieved value. + * @see Infocyph\ArrayKit\Array\DotNotation::get() + */ + public static function offsetGet(array $array, string $key): mixed + { + return static::get($array, $key); + } + + /** + * Set a value in the array using dot notation. + * + * This method is a part of the ArrayAccess implementation. + * + * @param array &$array The array to set the value in. + * @param string $key The dot-notation key to set. + * @param mixed $value The value to set. * @return void + * @see Infocyph\ArrayKit\Array\DotNotation::set() */ - private static function setValue(array &$array, string $key, mixed $value): void + public static function offsetSet(array &$array, string $key, mixed $value): void { - $keys = explode('.', $key); - $count = count($keys); + static::set($array, $key, $value); + } + + /** + * Unset a value in the array using dot notation. + * + * This method removes a value from the provided array + * at the specified dot-notation key. It leverages the + * forget logic to handle nested arrays and supports + * wildcard paths. + * + * @param array &$array The array from which to unset the value. + * @param string $key The dot-notation key of the value to unset. + * @return void + */ + public static function offsetUnset(array &$array, string $key): void + { + static::forget($array, $key); + } + + /** + * Retrieve a value from the array using dot notation. + * + * This method supports retrieving values from the given array + * using dot-notation keys. It will traverse the array as necessary + * to retrieve the value. If the key is not found, the default value + * is returned. + * + * @param array $target The array to retrieve the value from. + * @param int|string $key The key to retrieve (supports dot notation). + * @param mixed $default The default value to return if the key is not found. + * @return mixed The retrieved value. + */ + private static function getValue(array $target, int|string $key, mixed $default): mixed + { + if (is_int($key) || ArraySingle::exists($target, $key)) { + // Return top-level or integer index + return $target[$key] ?? static::value($default); + } + if (!is_string($key) || !str_contains($key, '.')) { + // If no dot path + return static::value($default); + } - foreach ($keys as $i => $segment) { - if ($count - $i === 1) { - break; + return static::traverseGet($target, explode('.', $key), $default); + } + + + + /** + * Traverses the target array/object to retrieve a value using dot notation. + * + * This method is called recursively by the `get` method to traverse the given + * array or object using dot notation. It expects the target array or object, + * the segments of the dot-notation key, and the default value to return if + * the key is not found. + * + * @param mixed $target The array or object to traverse. + * @param array $segments The segments of the dot-notation key. + * @param mixed $default The default value to return if the key is not found. + * @return mixed The retrieved value. + */ + private static function traverseGet(mixed $target, array $segments, mixed $default): mixed + { + foreach ($segments as $i => $segment) { + unset($segments[$i]); + + if ($segment === null) { + return $target; + } + + if ($segment === '*') { + return static::traverseWildcard($target, $segments, $default); } - if (!isset($array[$segment]) || !is_array($array[$segment])) { - $array[$segment] = []; + + $normalized = static::normalizeSegment($segment, $target); + $target = static::accessSegment($target, $normalized, $default); + if ($target === static::value($default)) { + return static::value($default); } - $array = &$array[$segment]; } + return $target; + } + + + /** + * Normalize a dot-notation segment by replacing escaped values and resolving + * special values such as '{first}' and '{last}'. + * + * @param string $segment The segment of the dot-notation key. + * @param mixed $target The target array or object to resolve against. + * @return mixed The normalized segment. + */ + private static function normalizeSegment(string $segment, mixed $target): mixed + { + return match ($segment) { + '\\*' => '*', + '\\{first}' => '{first}', + '{first}' => static::resolveFirst($target), + '\\{last}' => '{last}', + '{last}' => static::resolveLast($target), + default => $segment, + }; + } + - $array[$keys[$count - 1]] = $value; + /** + * Access a segment in a target array or object. + * + * This method takes a target array or object, a segment, and a default value. + * It returns the value of the segment in the target if it exists, or the default + * value if it does not. It supports both array and object access. Array access + * is attempted first, then object access. + * + * @param mixed $target The target array or object to access. + * @param mixed $segment The segment to access. + * @param mixed $default The default value to return if the segment does not exist. + * @return mixed The value of the segment in the target, or the default value. + */ + private static function accessSegment(mixed $target, mixed $segment, mixed $default): mixed + { + return match (true) { + BaseArrayHelper::accessible($target) && ArraySingle::exists($target, $segment) + => $target[$segment], + is_object($target) && isset($target->{$segment}) + => $target->{$segment}, + default => static::value($default), + }; } + /** - * Internal helper to get a nested array item using dot notation. + * Traverse a target array/object using dot-notation with wildcard support. * - * @param array $array - * @param array|int|string $key - * @param mixed|null $default - * @return mixed + * This method handles cases where a wildcard ('*') is present in the dot-notation key. + * It iterates over each element of the target, applying the remaining segments to retrieve + * the specified value. If segments contain another wildcard, the results are collapsed into + * a single array. If the target is not accessible, the default value is returned. + * + * @param mixed $target The array or object to traverse. + * @param array $segments The segments of the dot-notation key, including potential wildcards. + * @param mixed $default The default value to return if the key is not found. + * @return mixed The retrieved value(s) from the target based on the dot-notation key. */ - private static function getValue(array $array, array|int|string $key, mixed $default = null): mixed + private static function traverseWildcard(mixed $target, array $segments, mixed $default): mixed { - if (is_int($key) || ArraySingle::exists($array, $key)) { - return $array[$key] ?? $default; + $target = method_exists($target, 'all') ? $target->all() : $target; + if (!BaseArrayHelper::accessible($target)) { + return static::value($default); } - if (!is_string($key) || !str_contains($key, '.')) { - return $default; + $result = []; + foreach ($target as $item) { + $result[] = static::traverseGet($item, $segments, $default); + } + if (in_array('*', $segments, true)) { + $result = ArrayMulti::collapse($result); + } + return $result; + } + + + /** + * Sets a value in the target array/object using dot notation. + * + * This method sets a value in the target array or object using dot notation. + * It supports wildcard and dot notation for nested arrays or objects. + * If the segment path is not fully defined within the target array, + * it will create nested arrays as necessary. If the `overwrite` flag is true, + * it will replace any existing value at the final segment; otherwise, + * it will only set the value if the property does not already exist. + * + * @param mixed &$target The target array or object to set the value in. + * @param string $key The dot-notation key of the value to set. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite any existing value. + * @return void + */ + private static function setValue(mixed &$target, string $key, mixed $value, bool $overwrite): void + { + $segments = explode('.', $key); + $first = array_shift($segments); + + if ($first === '*') { + static::handleWildcardSet($target, $segments, $value, $overwrite); + return; } - return static::segment($array, $key, $default); + if (BaseArrayHelper::accessible($target)) { + static::setValueArray($target, $first, $segments, $value, $overwrite); + } elseif (is_object($target)) { + static::setValueObject($target, $first, $segments, $value, $overwrite); + } else { + static::setValueFallback($target, $first, $segments, $value, $overwrite); + } } + /** - * Internal helper to traverse an array by "dot" segments. + * Sets values in the target using dot-notation with wildcard support. * - * @param array $array - * @param string $key - * @param mixed $default - * @return mixed + * This method handles cases where the first segment in the dot-notation key + * is a wildcard ('*'). It iterates over each element of the target, applying + * the remaining segments to set the specified value. If segments are present, + * it continues setting values recursively. If the overwrite flag is true and + * no segments remain, it sets each element in the target to the provided value. + * + * @param mixed &$target The target to set values in, typically an array. + * @param array $segments The remaining segments of the dot-notation key. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite existing values. + * @return void + */ + private static function handleWildcardSet(mixed &$target, array $segments, mixed $value, bool $overwrite): void + { + if (!BaseArrayHelper::accessible($target)) { + $target = []; + } + if (!empty($segments)) { + foreach ($target as &$inner) { + static::setValue($inner, implode('.', $segments), $value, $overwrite); + } + } elseif ($overwrite) { + foreach ($target as &$inner) { + $inner = $value; + } + } + } + + + /** + * Sets a value in the target array using dot-notation segments. + * + * If the segment path is not fully defined within the target array, + * it will create nested arrays as necessary. If the `overwrite` flag is + * true, it will replace any existing value at the final segment; + * otherwise, it will only set the value if the property does not + * already exist. + * + * @param array &$target The target array to set the value in. + * @param string $segment The current segment of the dot-notation key. + * @param array $segments The remaining segments of the dot-notation key. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite any existing value. + * @return void */ - private static function segment(array $array, string $key, mixed $default = null): mixed + private static function setValueArray(array &$target, string $segment, array $segments, mixed $value, bool $overwrite): void { - foreach (explode('.', $key) as $segment) { - if (is_array($array) && ArraySingle::exists($array, $segment)) { - $array = $array[$segment]; + if (!empty($segments)) { + if (!ArraySingle::exists($target, $segment)) { + $target[$segment] = []; + } + static::setValue($target[$segment], implode('.', $segments), $value, $overwrite); + } else { + if ($overwrite || !ArraySingle::exists($target, $segment)) { + $target[$segment] = $value; + } + } + } + + + /** + * Sets a value in an object using dot-notation segments. + * + * This function is responsible for setting a value in a given object + * by traversing the object's properties using dot-notation segments. + * If the segment path is not fully defined within the object, it will + * create nested arrays as necessary. If the `overwrite` flag is true, + * it will replace any existing value at the final segment; otherwise, + * it will only set the value if the property does not already exist. + * + * @param object &$target The object to set the value in. + * @param string $segment The current segment of the dot-notation key. + * @param array $segments The remaining segments of the dot-notation key. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite any existing value. + * @return void + */ + private static function setValueObject(object &$target, string $segment, array $segments, mixed $value, bool $overwrite): void + { + if (!empty($segments)) { + if (!isset($target->{$segment})) { + $target->{$segment} = []; + } + static::setValue($target->{$segment}, implode('.', $segments), $value, $overwrite); + } else { + if ($overwrite || !isset($target->{$segment})) { + $target->{$segment} = $value; + } + } + } + + + /** + * Sets a value in a target that is not an array or object. + * + * This function is called when the target is not an array or object. + * It creates an array and sets the value in the array. + * + * @param mixed &$target The target to set the value in. + * @param string $segment The segment of the dot-notation key. + * @param array $segments The segments of the dot-notation key. + * @param mixed $value The value to set. + * @param bool $overwrite If true, overwrite any existing value. + * @return void + */ + private static function setValueFallback(mixed &$target, string $segment, array $segments, mixed $value, bool $overwrite): void + { + $target = []; + if (!empty($segments)) { + static::setValue($target[$segment], implode('.', $segments), $value, $overwrite); + } elseif ($overwrite) { + $target[$segment] = $value; + } + } + + + /** + * Retrieve a value from an array using an exact key path. + * + * If the key path is not found, the default value is returned. + * + * @param mixed $array The array to retrieve the value from. + * @param string $path The key path to use for retrieval. + * @param mixed $default The default value to return if the key path is not found. + * @return mixed The retrieved value or default value. + */ + private static function segmentExact(mixed $array, string $path, mixed $default): mixed + { + if (!str_contains($path, '.')) { + return ArraySingle::exists($array, $path) ? $array[$path] : $default; + } + $parts = explode('.', $path); + foreach ($parts as $part) { + if (is_array($array) && ArraySingle::exists($array, $part)) { + $array = $array[$part]; } else { return $default; } } - return $array; } + + + /** + * Resolve the {first} segment for an array-like target. + * + * @param mixed $target An array or collection-like object. + * @return string|int|null The first key in the array or collection, or '{first}' if not resolved. + */ + private static function resolveFirst(mixed $target): string|int|null + { + if (method_exists($target, 'all')) { + $arr = $target->all(); + return array_key_first($arr); + } elseif (is_array($target)) { + return array_key_first($target); + } + return '{first}'; + } + + + /** + * Resolves the {last} segment for an array-like target. + * + * @param mixed $target An array or collection-like object. + * @return string|int|null The last key in the array or collection, or '{last}' if not resolved. + */ + private static function resolveLast(mixed $target): string|int|null + { + if (method_exists($target, 'all')) { + $arr = $target->all(); + return array_key_last($arr); + } elseif (is_array($target)) { + return array_key_last($target); + } + return '{last}'; + } + + + /** + * Returns the given value if it's not a callable, otherwise calls it and returns the result. + * + * @param mixed $val + * @return mixed + */ + private static function value(mixed $val): mixed + { + return is_callable($val) ? $val() : $val; + } + + } diff --git a/src/Collection/BaseCollectionTrait.php b/src/Collection/BaseCollectionTrait.php new file mode 100644 index 0000000..2545747 --- /dev/null +++ b/src/Collection/BaseCollectionTrait.php @@ -0,0 +1,315 @@ +data = $data; + } + + /** + * Create a new collection instance from any arrayable input. + */ + public static function make(mixed $data): static + { + $instance = new static([]); + $instance->data = $instance->getArrayableItems($data); + + return $instance; + } + + /** + * Magic method __call to delegate undefined method calls. + * + * If a method isn't defined on the collection, the call is forwarded to + * a new Pipeline instance (which offers a rich, chainable API). + * + * @return Pipeline|mixed + * + * @throws BadMethodCallException + */ + public function __call(string $method, array $arguments): mixed + { + $pipeline = $this->process(); + if (method_exists($pipeline, $method)) { + return $pipeline->$method(...$arguments); + } + throw new BadMethodCallException("Method $method does not exist in ".static::class); + } + + /** + * Magic method __invoke allows the instance to be called as a function. + * + * When the collection object is used as a function, it returns the underlying data array. + */ + public function __invoke(): array + { + return $this->data; + } + + /** + * Create and return a new Pipeline instance using the current collection's data. + * + * This method initializes a processing pipeline, allowing method chaining + * for array transformations or operations. + * + * @return Pipeline A new pipeline instance for further processing. + */ + public function process(): Pipeline + { + return $this->pipeline ??= new Pipeline($this->data, $this); + } + + /** + * Set the underlying array data for the collection. + * This method is chainable. + * + * @param array $data The new data to set. + * @return static The current collection instance, for chaining. + */ + public function setData(array $data): static + { + $this->data = $data; + + return $this; + } + + /** + * Retrieve the current collection instance. + * + * This method returns the current collection object itself, allowing + * for further method chaining or operations on the existing collection. + * + * @return static The current collection instance. + */ + public function get(): static + { + return $this; + } + + /** + * Magic getter to retrieve an item via property access: $collection->key + */ + public function __get(string $key): mixed + { + return $this->offsetGet($key); + } + + /** + * Magic setter to set an item via property access: $collection->key = value + */ + public function __set(string $key, mixed $value): void + { + $this->offsetSet($key, $value); + } + + /** + * Magic isset to check existence of an item via property access: isset($collection->key) + */ + public function __isset(string $key): bool + { + return $this->offsetExists($key); + } + + /** + * Magic unset to remove an item via property access: unset($collection->key) + */ + public function __unset(string $key): void + { + $this->offsetUnset($key); + } + + /** + * Convert various structures (collections, Traversable, etc.) to an array. + */ + public function getArrayableItems(mixed $items): array + { + return match (true) { + $items instanceof self => $items->items(), + $items instanceof JsonSerializable => $items->jsonSerialize(), + $items instanceof Traversable => iterator_to_array($items), + default => (array) $items, + }; + } + + /** + * Return the raw array of items in this collection. + */ + public function items(): array + { + return $this->data; + } + + /** + * Get the collection of items as a JSON string. + * + * @param int $options JSON encoding options + */ + public function toJson(int $options = 0): string + { + return json_encode($this->data, $options); + } + + /** + * Determine if the collection is empty. + */ + public function isEmpty(): bool + { + return empty($this->data); + } + + /** + * Convert the collection to a JSON string when treated as a string. + */ + public function __toString(): string + { + return $this->toJson(); + } + + /** + * Get the collection of items as a plain array. + */ + public function toArray(): array + { + return $this->data; + } + + /** + * Return an array of all the keys in the collection. + */ + public function keys(): array + { + return array_keys($this->data); + } + + /** + * Provide custom debug information. + */ + public function __debugInfo(): array + { + return [ + 'data' => $this->data, + 'count' => $this->count(), + ]; + } + + /** + * Clear all items from the collection. + */ + public function clear(): void + { + $this->data = []; + } + + /* + |-------------------------------------------------------------------------- + | ArrayAccess Interface + |-------------------------------------------------------------------------- + */ + + public function offsetExists(mixed $offset): bool + { + return isset($this->data[$offset]) || array_key_exists($offset, $this->data); + } + + public function offsetGet(mixed $offset): mixed + { + return $this->data[$offset] ?? null; + } + + public function offsetSet(mixed $offset, mixed $value): void + { + if ($offset === null) { + $this->data[] = $value; + } else { + $this->data[$offset] = $value; + } + } + + public function offsetUnset(mixed $offset): void + { + unset($this->data[$offset]); + } + + /* + |-------------------------------------------------------------------------- + | Iterator Interface + |-------------------------------------------------------------------------- + */ + + public function getIterator(): Traversable + { + return new ArrayIterator($this->data); + } + + public function current(): mixed + { + return current($this->data); + } + + public function key(): string|int|null + { + return key($this->data); + } + + public function next(): void + { + next($this->data); + } + + public function valid(): bool + { + return key($this->data) !== null; + } + + public function rewind(): void + { + reset($this->data); + } + + /* + |-------------------------------------------------------------------------- + | Countable Interface + |-------------------------------------------------------------------------- + */ + + public function count(): int + { + return count($this->data); + } + + /* + |-------------------------------------------------------------------------- + | JsonSerializable Interface + |-------------------------------------------------------------------------- + */ + + public function jsonSerialize(): array + { + return array_map( + static fn ($value) => $value instanceof JsonSerializable ? $value->jsonSerialize() : $value, + $this->data, + ); + } +} diff --git a/src/Functional/BucketCollection.php b/src/Collection/Collection.php similarity index 74% rename from src/Functional/BucketCollection.php rename to src/Collection/Collection.php index 9ca2b15..7086db2 100644 --- a/src/Functional/BucketCollection.php +++ b/src/Collection/Collection.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Infocyph\ArrayKit\Functional; +namespace Infocyph\ArrayKit\Collection; use ArrayAccess; use Countable; @@ -16,7 +16,7 @@ * interfaces (ArrayAccess, Iterator, Countable, JsonSerializable). * Inherits most of its behavior from BaseCollectionTrait. */ -class BucketCollection implements ArrayAccess, Iterator, Countable, JsonSerializable +class Collection implements ArrayAccess, Countable, Iterator, JsonSerializable { use BaseCollectionTrait; } diff --git a/src/Functional/HookedCollection.php b/src/Collection/HookedCollection.php similarity index 97% rename from src/Functional/HookedCollection.php rename to src/Collection/HookedCollection.php index 1000237..c9a1833 100644 --- a/src/Functional/HookedCollection.php +++ b/src/Collection/HookedCollection.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Infocyph\ArrayKit\Functional; +namespace Infocyph\ArrayKit\Collection; use ArrayAccess; use Countable; diff --git a/src/Collection/Pipeline.php b/src/Collection/Pipeline.php new file mode 100644 index 0000000..530a4d6 --- /dev/null +++ b/src/Collection/Pipeline.php @@ -0,0 +1,400 @@ +collection->setData($this->working); + } + + /* + |-------------------------------------------------------------------------- + | ArraySingle-based chainable methods (Single-Dimensional usage) + |-------------------------------------------------------------------------- + */ + + /** + * Keep only certain keys in the array, using ArraySingle::only. + * (Typically relevant if the array is associative 1D.) + */ + public function only(array|string $keys): static + { + $this->working = ArraySingle::only($this->working, $keys); + return $this; + } + + /** + * Return every n-th element, using ArraySingle::nth. + */ + public function nth(int $step, int $offset = 0): static + { + $this->working = ArraySingle::nth($this->working, $step, $offset); + return $this; + } + + /** + * Keep only duplicate values, using ArraySingle::duplicates. + * (Typically this means setting $this->working to the *list of duplicates*.) + */ + public function duplicates(): static + { + // If you want to *replace* the original array with only duplicates: + $dupes = ArraySingle::duplicates($this->working); + // This means our collection now becomes an array of those duplicated values. + // Possibly you might want to keep them in a "counts" structure, but let's do direct. + $this->working = $dupes; + return $this; + } + + /** + * Slice the array (like array_slice) using ArraySingle::slice. + */ + public function slice(int $offset, ?int $length = null): static + { + $this->working = ArraySingle::slice($this->working, $offset, $length); + return $this; + } + + /** + * "Paginate" the array by slicing it into a smaller segment, using ArraySingle::paginate. + */ + public function paginate(int $page, int $perPage): static + { + $this->working = ArraySingle::paginate($this->working, $page, $perPage); + return $this; + } + + /** + * Combine the current array with a second array of values, using ArraySingle::combine. + * (We treat $this->working as the *keys*, user passes an array of values.) + */ + public function combine(array $values): static + { + $combined = ArraySingle::combine($this->working, $values); + // Replacing the entire array with the combined result + $this->working = $combined; + return $this; + } + + /** + * Map each element, updating $this->working. (From prior example) + */ + public function map(callable $callback): static + { + $this->working = ArraySingle::map($this->working, $callback); + return $this; + } + + /** + * Filter the array using a callback, same as your prior "filter" example. + */ + public function filter(callable $callback): static + { + // We use "where(...)" or direct array_filter: + $this->working = ArraySingle::where($this->working, $callback); + return $this; + } + + /** + * chunk the array (single-dim). + */ + public function chunk(int $size, bool $preserveKeys = false): static + { + $this->working = ArraySingle::chunk($this->working, $size, $preserveKeys); + return $this; + } + + /** + * Return only unique values using ArraySingle::unique. + */ + public function unique(bool $strict = false): static + { + $this->working = ArraySingle::unique($this->working, $strict); + return $this; + } + + /** + * Reject certain items (inverse of filter), using ArraySingle::reject + */ + public function reject(mixed $callback = true): static + { + $this->working = ArraySingle::reject($this->working, $callback); + return $this; + } + + /** + * Skip the first N items. + */ + public function skip(int $count): static + { + $this->working = ArraySingle::skip($this->working, $count); + return $this; + } + + /** + * Skip items while callback returns true; once false, keep remainder. + */ + public function skipWhile(callable $callback): static + { + $this->working = ArraySingle::skipWhile($this->working, $callback); + return $this; + } + + /** + * Skip items until callback returns true, then keep remainder. + */ + public function skipUntil(callable $callback): static + { + $this->working = ArraySingle::skipUntil($this->working, $callback); + return $this; + } + + /** + * Partition [passed, failed]. + */ + public function partition(callable $callback): static + { + $this->working = ArraySingle::partition($this->working, $callback); + return $this; + } + + /* + |-------------------------------------------------------------------------- + | ArrayMulti-based chainable methods (Multi-Dimensional usage) + |-------------------------------------------------------------------------- + */ + + /** + * Flatten the array using ArrayMulti::flatten. + */ + public function flatten(float|int $depth = \INF): static + { + $this->working = ArrayMulti::flatten($this->working, $depth); + return $this; + } + + /** + * Flatten the array into a single level but preserve keys, using flattenByKey. + */ + public function flattenByKey(): static + { + $this->working = ArrayMulti::flattenByKey($this->working); + return $this; + } + + /** + * Recursively sort the array by keys/values, using ArrayMulti::sortRecursive. + */ + public function sortRecursive(int $options = SORT_REGULAR, bool $descending = false): static + { + $this->working = ArrayMulti::sortRecursive($this->working, $options, $descending); + return $this; + } + + /** + * Collapses an array of arrays into a single (1D) array (2D -> 1D). + */ + public function collapse(): static + { + $this->working = ArrayMulti::collapse($this->working); + return $this; + } + + /** + * Group a 2D array by a given column or callback, using ArrayMulti::groupBy. + */ + public function groupBy(string|callable $groupBy, bool $preserveKeys = false): static + { + $this->working = ArrayMulti::groupBy($this->working, $groupBy, $preserveKeys); + return $this; + } + + /** + * Filter rows where a certain key is between two values, using ArrayMulti::between. + */ + public function between(string $key, float|int $from, float|int $to): static + { + $this->working = ArrayMulti::between($this->working, $key, $from, $to); + return $this; + } + + /** + * Filter using a custom callback on each row, using ArrayMulti::whereCallback. + */ + public function whereCallback(?callable $callback = null, mixed $default = null): static + { + $this->working = ArrayMulti::whereCallback($this->working, $callback, $default); + return $this; + } + + /** + * Filter rows by a single key's comparison (like ->where('age', '>', 18)). + */ + public function where(string $key, mixed $operator = null, mixed $value = null): static + { + $this->working = ArrayMulti::where($this->working, $key, $operator, $value); + return $this; + } + + /** + * Filter rows where "column" matches one of the given values. + */ + public function whereIn(string $key, array $values, bool $strict = false): static + { + $this->working = ArrayMulti::whereIn($this->working, $key, $values, $strict); + return $this; + } + + /** + * Filter rows where "column" is not in the given values. + */ + public function whereNotIn(string $key, array $values, bool $strict = false): static + { + $this->working = ArrayMulti::whereNotIn($this->working, $key, $values, $strict); + return $this; + } + + /** + * Filter rows where a column is null, using ArrayMulti::whereNull. + */ + public function whereNull(string $key): static + { + $this->working = ArrayMulti::whereNull($this->working, $key); + return $this; + } + + /** + * Filter rows where a column is NOT null, using ArrayMulti::whereNotNull. + */ + public function whereNotNull(string $key): static + { + $this->working = ArrayMulti::whereNotNull($this->working, $key); + return $this; + } + + /** + * Sort by a column or callback in ascending/descending order. + */ + public function sortBy(string|callable $by, bool $desc = false, int $options = SORT_REGULAR): static + { + $this->working = ArrayMulti::sortBy($this->working, $by, $desc, $options); + return $this; + } + + /* + |-------------------------------------------------------------------------- + | BaseArrayHelper-based chainable or one-time checks + |-------------------------------------------------------------------------- + */ + + /** + * Check if the current array is multiDimensional, from BaseArrayHelper (not chainable). + */ + public function isMultiDimensional(): bool + { + return BaseArrayHelper::isMultiDimensional($this->working); + } + + /** + * Wrap the entire array if it's not already an array, from BaseArrayHelper::wrap + */ + public function wrap(): static + { + $this->working = BaseArrayHelper::wrap($this->working); + return $this; + } + + /** + * Example: Unwrap an array if it has exactly one element, from BaseArrayHelper::unWrap. + */ + public function unWrap(): static + { + // Might produce a non-array, so up to you if you want to store that as $working... + $unwrapped = BaseArrayHelper::unWrap($this->working); + // If $unwrapped is not array, we store it as a single-element array to keep chain consistent + $this->working = is_array($unwrapped) ? $unwrapped : [$unwrapped]; + return $this; + } + + /** + * Shuffle the array in place, from ArraySingle::shuffle or BaseArrayHelper logic. + */ + public function shuffle(?int $seed = null): static + { + $this->working = ArraySingle::shuffle($this->working, $seed); + return $this; + } + + /* + |-------------------------------------------------------------------------- + | Additional Non-Chain Methods That Return A Single Value + |-------------------------------------------------------------------------- + */ + + /** + * Return the sum of the current array (for single-dim usage). + * Not chainable, it ends the pipeline by returning a numeric. + */ + public function sum(?callable $callback = null): float|int + { + return ArraySingle::sum($this->working, $callback); + } + + /** + * Return the first item in a 2D array, or single-dim array, depending on usage. + * from ArrayMulti::first or direct approach. + */ + public function first(?callable $callback = null, mixed $default = null): mixed + { + return ArrayMulti::first($this->working, $callback, $default); + } + + /** + * Return the last item in a 2D array, or single-dim array, depending on usage. + */ + public function last(?callable $callback = null, mixed $default = null): mixed + { + return ArrayMulti::last($this->working, $callback, $default); + } + + /** + * Return a "reduced" single value from the array (like sum-of-squares), from ArraySingle::reduce. + */ + public function reduce(callable $callback, mixed $initial = null): mixed + { + return ArraySingle::reduce($this->working, $callback, $initial); + } + + /** + * Quick example: Check if at least one item passes a truth test, from ArraySingle::some or ArrayMulti::some + * Not chainable, returns bool. + */ + public function any(callable $callback): bool + { + return ArraySingle::some($this->working, $callback); + } +} diff --git a/src/Config/BaseConfigTrait.php b/src/Config/BaseConfigTrait.php index 37bc402..4de17c8 100644 --- a/src/Config/BaseConfigTrait.php +++ b/src/Config/BaseConfigTrait.php @@ -6,12 +6,6 @@ use Infocyph\ArrayKit\Array\DotNotation; -/** - * Trait BaseConfigTrait - * - * Provides shared methods for loading and managing configuration items, - * including checking if keys exist, retrieving values, and setting values. - */ trait BaseConfigTrait { /** @@ -60,7 +54,7 @@ public function all(): array } /** - * Check if one or multiple keys exist in the configuration. + * Check if one or multiple keys exist in the configuration (no wildcard). * * @param string|array $keys Dot-notation key(s) * @return bool True if the key(s) exist @@ -70,10 +64,22 @@ public function has(string|array $keys): bool return DotNotation::has($this->items, $keys); } + /** + * Check if *any* of the given keys exist (no wildcard). + * + * @param string|array $keys Dot-notation key(s) + * @return bool True if at least one key exists + */ + public function hasAny(string|array $keys): bool + { + return DotNotation::hasAny($this->items, $keys); + } + /** * Get one or multiple items from the configuration. + * Includes wildcard support (e.g. '*'), {first}, {last}, etc. * - * @param string|int|array|null $key Dot-notation key(s) or null for entire config + * @param string|int|array|null $key Dot-notation key(s) or null for entire config * @param mixed|null $default Default value if key not found * @return mixed The value(s) found or default */ @@ -83,21 +89,49 @@ public function get(string|int|array $key = null, mixed $default = null): mixed } /** - * Set a configuration value by dot-notation key. + * Set a configuration value by dot-notation key (wildcard support), + * optionally controlling overwrite vs. fill-like behavior. * * If no key is provided, replaces the entire config array with $value. * * @param string|array|null $key Dot-notation key or [key => value] array - * @param mixed|null $value The value to set + * @param mixed|null $value The value to set + * @param bool $overwrite Overwrite existing? Default true. * @return bool True on success */ - public function set(string|array|null $key = null, mixed $value = null): bool + public function set(string|array|null $key = null, mixed $value = null, bool $overwrite = true): bool + { + return DotNotation::set($this->items, $key, $value, $overwrite); + } + + /** + * "Fill" config data where it's missing, i.e. DotNotation's fill logic. + * + * @param string|array $key Dot-notation key or multiple [key => value] + * @param mixed|null $value The value to set if missing + * @return bool + */ + public function fill(string|array $key, mixed $value = null): bool + { + DotNotation::fill($this->items, $key, $value); + return true; + } + + /** + * Remove/unset a key (or keys) from configuration using dot notation + wildcard expansions. + * + * @param string|int|array $key + * @return bool + */ + public function forget(string|int|array $key): bool { - return DotNotation::set($this->items, $key, $value); + DotNotation::forget($this->items, $key); + return true; } /** * Prepend a value to a configuration array at the specified key. + * (No direct wildcard usage, though underlying DotNotation can handle it if needed.) * * @param string $key The dot-notation key referencing an array * @param mixed $value The value to prepend diff --git a/src/Config/DynamicConfig.php b/src/Config/DynamicConfig.php index 74c587a..22d8a93 100644 --- a/src/Config/DynamicConfig.php +++ b/src/Config/DynamicConfig.php @@ -7,48 +7,54 @@ use Infocyph\ArrayKit\Array\DotNotation; use Infocyph\ArrayKit\traits\HookTrait; -/** - * Class DynamicConfig - * - * Provides dynamic configuration handling with hooks for - * "on get" and "on set" value transformations. - * Inherits base config operations from BaseConfigTrait - * and advanced multi-config features from the Multi trait. - */ class DynamicConfig { use BaseConfigTrait; use HookTrait; + /** - * Retrieve a configuration value by dot-notation key, applying any "on get" hooks. + * Retrieves a configuration value by dot-notation key, applying any "on get" hooks. * - * @param int|string|null $key Dot-notation key (or null for entire config) - * @param mixed|null $default Default value if key not found - * @return mixed + * @param int|string|null $key The key to retrieve (supports dot notation) + * @param mixed $default The default value to return if the key is not found + * @return mixed The retrieved value */ public function get(int|string $key = null, mixed $default = null): mixed { - // First retrieve from the config array $value = DotNotation::get($this->items, $key, $default); - - // Then apply any "on get" hook transformations return $this->processValue($key, $value, 'get'); } + /** - * Set a configuration value by dot-notation key, applying any "on set" hooks. + * Sets a configuration value by dot-notation key, applying any "on set" hooks. * - * @param string|null $key Dot-notation key (null replaces entire config) - * @param mixed|null $value The value to set + * @param string|null $key The key to set (supports dot notation) + * @param mixed $value The value to set + * @param bool $overwrite If true, overwrite existing values; otherwise, fill in missing (default true) * @return bool True on success */ - public function set(?string $key = null, mixed $value = null): bool + public function set(?string $key = null, mixed $value = null, bool $overwrite = true): bool { - // Apply "on set" hook transformations + // The user might want the dynamic config to also accept $overwrite param for fill-like usage $processedValue = $this->processValue($key, $value, 'set'); + return DotNotation::set($this->items, $key, $processedValue, $overwrite); + } + - // Update the config array using DotNotation - return DotNotation::set($this->items, $key, $processedValue); + /** + * "Fill" config data where it's missing, i.e. DotNotation's fill logic, + * applying any "on set" hooks to the value. + * + * @param string|array $key Dot-notation key or multiple [key => value] + * @param mixed|null $value The value to set if missing + * @return bool True on success + */ + public function fill(string|array $key, mixed $value = null): bool + { + $processed = $this->processValue($key, $value, 'set'); + DotNotation::fill($this->items, $key, $processed); + return true; } } diff --git a/src/Functional/BaseCollectionTrait.php b/src/Functional/BaseCollectionTrait.php deleted file mode 100644 index 216dd91..0000000 --- a/src/Functional/BaseCollectionTrait.php +++ /dev/null @@ -1,431 +0,0 @@ -data = $data; - } - - /** - * Magic getter to retrieve an item via property access: $collection->key - * - * @param string $key - * @return mixed - */ - public function __get(string $key): mixed - { - return $this->offsetGet($key); - } - - /** - * Magic setter to set an item via property access: $collection->key = value - * - * @param string $key - * @param mixed $value - */ - public function __set(string $key, mixed $value): void - { - $this->offsetSet($key, $value); - } - - /** - * Magic isset to check existence of an item via property access: isset($collection->key) - * - * @param string $key - * @return bool - */ - public function __isset(string $key): bool - { - return $this->offsetExists($key); - } - - /** - * Magic unset to remove an item via property access: unset($collection->key) - * - * @param string $key - */ - public function __unset(string $key): void - { - $this->offsetUnset($key); - } - - /** - * Merge another collection or array into this one. - * - * @param BucketCollection|HookedCollection|array $data Another collection instance or raw array - * @return static - */ - public function merge(BucketCollection|HookedCollection|array $data): static - { - // Convert collection to array if needed - if ($data instanceof BucketCollection || $data instanceof HookedCollection) { - $data = $data->items(); - } - - $this->data = array_merge($this->data, $data); - return $this; - } - - /** - * Map each item using a callback and return the resulting array (not a new collection). - * - * @param callable $callback fn($value, $key): mixed - * @return array - */ - public function map(callable $callback): array - { - return array_map($callback, $this->data); - } - - /** - * Create a new collection instance from an array after mapping each item with a callback. - * - * @param array $items An array of raw items - * @param callable $fn fn($value, $key): mixed - * @return static - */ - public static function fromMap(array $items, callable $fn): static - { - return new static(array_map($fn, $items)); - } - - /** - * Reduce the collection to a single value using a callback. - * - * @param callable $fn fn($carry, $value): mixed - * @param mixed $initial Initial accumulator value - * @return mixed - */ - public function reduce(callable $fn, mixed $initial): mixed - { - return array_reduce($this->data, $fn, $initial); - } - - /** - * Apply a callback to each item of the collection. - * - * @param callable $fn fn($value, $key): void - */ - public function each(callable $fn): void - { - array_walk($this->data, $fn); - } - - /** - * Check if at least one item in the collection passes the callback test. - * - * @param callable $fn fn($value, $key, $allData): bool - * @return bool - */ - public function exists(callable $fn): bool - { - foreach ($this->data as $index => $element) { - if ($fn($element, $index, $this->data)) { - return true; - } - } - return false; - } - - /** - * Filter the collection by a callback, returning a new collection instance. - * - * @param callable $fn fn($value, $key): bool - * @return static A new collection with filtered items - */ - public function filter(callable $fn): static - { - $filtered = array_filter($this->data, $fn, ARRAY_FILTER_USE_BOTH); - return new static($filtered); - } - - /** - * Return the first item in the collection. - * - * @return mixed - */ - public function first(): mixed - { - return reset($this->data); - } - - /** - * Return the last item in the collection. - * - * @return mixed - */ - public function last(): mixed - { - return end($this->data); - } - - /** - * Convert various structures (collections, Traversable, etc.) to an array. - * - * @param mixed $items - * @return array - */ - public function getArrayableItems(mixed $items): array - { - return match (true) { - $items instanceof self => $items->items(), - $items instanceof JsonSerializable => $items->jsonSerialize(), - $items instanceof Traversable => iterator_to_array($items), - default => (array) $items - }; - } - - /** - * Replace the internal data with a new array. - * - * @param array $data - * @return static - */ - public function setData(array $data): static - { - $this->data = $data; - return $this; - } - - /** - * Get all values from the collection (without keys). - * - * @return array - */ - public function values(): array - { - return array_values($this->data); - } - - /** - * Return the raw array of items in this collection. - * - * @return array - */ - public function items(): array - { - return $this->data; - } - - /** - * Specify data which should be serialized to JSON. - * - * @return array - */ - public function jsonSerialize(): array - { - return array_map( - static function ($value) { - if ($value instanceof JsonSerializable) { - return $value->jsonSerialize(); - } - return $value; - }, - $this->data - ); - } - - /** - * Get the collection of items as a plain array (same as items()). - * - * @return array - */ - public function toArray(): array - { - // If you need a deeper serialization, you could do it here. - return array_map(static fn ($value) => $value, $this->data); - } - - /** - * Get the collection of items as a JSON string. - * - * @param int $options JSON encoding options - * @return string - */ - public function toJson(int $options = 0): string - { - return json_encode($this->data, $options); - } - - /** - * Convert the collection to a JSON string when treated as a string. - * - * @return string - */ - public function __toString(): string - { - return $this->toJson(); - } - - /** - * Determine if the collection is empty. - * - * @return bool - */ - public function isEmpty(): bool - { - return empty($this->data); - } - - /** - * Get an iterator for the items (for foreach loops). - * - * @return ArrayIterator - */ - public function getIterator(): ArrayIterator - { - return new ArrayIterator($this->data); - } - - /** - * ArrayAccess: Return the item at the given key/offset. - * - * @param mixed $offset - * @return mixed - */ - public function offsetGet(mixed $offset): mixed - { - return $this->data[$offset] ?? null; - } - - /** - * ArrayAccess: Set the item at the given key/offset. - * - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet(mixed $offset, mixed $value): void - { - if ($offset === null) { - $this->data[] = $value; - } else { - $this->data[$offset] = $value; - } - } - - /** - * ArrayAccess: Check if an item exists at the given offset. - * - * @param mixed $offset - * @return bool - */ - public function offsetExists(mixed $offset): bool - { - return isset($this->data[$offset]) || array_key_exists($offset, $this->data); - } - - /** - * ArrayAccess: Remove the item at the given offset. - * - * @param mixed $offset - */ - public function offsetUnset(mixed $offset): void - { - unset($this->data[$offset]); - } - - /** - * Iterator: Rewind the iterator to the first item. - */ - public function rewind(): void - { - reset($this->data); - } - - /** - * Iterator: Return the current item. - * - * @return mixed - */ - public function current(): mixed - { - return current($this->data); - } - - /** - * Iterator: Return the key of the current element. - * - * @return string|int|null - */ - public function key(): string|int|null - { - return key($this->data); - } - - - /** - * Iterator: Move forward to next element. - * - */ - public function next(): void - { - next($this->data); - } - - /** - * Iterator: Check if current position is valid. - * - * @return bool - */ - public function valid(): bool - { - return key($this->data) !== null; - } - - /** - * Countable: Return the number of items in the collection. - * - * @return int - */ - public function count(): int - { - return count($this->data); - } - - /** - * Return an array of all the keys in the collection. - * - * @return array - */ - public function keys(): array - { - return array_keys($this->data); - } - - /** - * Clear all items from the collection. - * - * @return void - */ - public function clear(): void - { - $this->data = []; - } -} diff --git a/src/functions.php b/src/functions.php index 31fc50a..fb4ee33 100644 --- a/src/functions.php +++ b/src/functions.php @@ -2,8 +2,6 @@ declare(strict_types=1); -use Infocyph\ArrayKit\Config\Config; -use Infocyph\ArrayKit\Config\DynamicConfig; if (!function_exists('compare')) { /** @@ -44,71 +42,3 @@ function isCallable(mixed $value): bool return !is_string($value) && is_callable($value); } } - -if (!function_exists('config')) { - /** - * Get/Set configuration via the Config class, or retrieve its singleton instance. - * - * Usage: - * - config(): returns the global Config instance. - * - config(['key' => 'value']): sets the config for 'key' to 'value'. - * - config('key'): gets the value of 'key'. - * - config('key', 'default'): gets the value of 'key', or 'default' if not found. - * - * @param array|int|string|null $keys If null, returns the Config instance. - * If array, sets those key/value pairs. - * If string|int, retrieves that key's value. - * @param mixed|null $default Default value if key not found - * @return Config|mixed - * @throws Exception - */ - function config(array|int|string $keys = null, mixed $default = null) - { - // If no arguments, return the Config instance - if ($keys === null) { - return Config::instance(); - } - - // If an array is passed, set each key => value - if (is_array($keys)) { - return Config::instance()->set($keys); - } - - // Otherwise, retrieve the value for the given key - return Config::instance()->get($keys, $default); - } -} - -if (!function_exists('formation')) { - /** - * Get/Set configuration using DynamicConfig or get its singleton instance. - * - * Usage: - * - formation(): returns the global DynamicConfig instance. - * - formation(['key' => 'value']): sets 'key' to 'value'. - * - formation('key'): retrieves the value of 'key'. - * - formation('key', 'default'): retrieves the value of 'key', or 'default' if missing. - * - * @param array|int|string|null $key The key(s) or null for the instance - * @param mixed|null $default Default value if key not found - * @return DynamicConfig|mixed - * @throws Exception - */ - function formation(array|int|string $key = null, mixed $default = null) - { - // If no arguments, return the DynamicConfig instance - if ($key === null) { - return DynamicConfig::instance(); - } - - // If an array is passed, we assume a single key => value pair - if (is_array($key)) { - // for consistency, let's set them all if it has multiple keys - // or if you only want to set the first key, preserve the old logic - return DynamicConfig::instance()->set(...$key); - } - - // Otherwise retrieve the key's value - return DynamicConfig::instance()->get($key, $default); - } -} diff --git a/tests/Feature/ArrayMultiTest.php b/tests/Feature/ArrayMultiTest.php index 7658b7b..bd78de9 100644 --- a/tests/Feature/ArrayMultiTest.php +++ b/tests/Feature/ArrayMultiTest.php @@ -41,3 +41,296 @@ ['a' => 4, 'z' => 1], ]); }); +// only() +it('selects only specified keys from a multidimensional array', function () { + $data = [ + ['a' => 1, 'b' => 2, 'c' => 3], + ['a' => 4, 'b' => 5, 'c' => 6], + ['a' => 7, 'b' => 8], + ]; + $result = ArrayMulti::only($data, ['a', 'c']); + expect($result)->toBe([ + ['a' => 1, 'c' => 3], + ['a' => 4, 'c' => 6], + ['a' => 7], + ]); +}); + +// collapse() +it('collapses a multidimensional array into a single-dimensional array', function () { + $data = [ + [1, 2], + [3, 4], + 5, + [6, 7], + ]; + $result = ArrayMulti::collapse($data); + expect($result)->toBe([1, 2, 3, 4, 6, 7]); +}); + +// depth() +it('calculates the depth of a multidimensional array', function () { + $data = [1, [2, 3], [[4]], []]; + // Depth: outermost is 1, [2,3] => depth 2, [[4]] => depth 3. + expect(ArrayMulti::depth($data))->toBe(3); +}); + +// flatten() complete flattening +it('fully flattens a multidimensional array when depth is INF', function () { + $data = [1, [2, [3, 4]], 5]; + $result = ArrayMulti::flatten($data); + expect($result)->toBe([1, 2, 3, 4, 5]); +}); + +// flatten() partial flattening +it('flattens a multidimensional array to a specified depth', function () { + $data = [1, [2, [3, 4]], 5]; + $result = ArrayMulti::flatten($data, 1); + // Only one level flattened: subarrays remain as-is. + expect($result)->toBe([1, 2, [3, 4], 5]); +}); + +// flattenByKey() +it('flattens an array while preserving keys', function () { + $data = [ + 'a' => [1, 2], + 'b' => [3, 4], + ]; + $result = ArrayMulti::flattenByKey($data); + // Because iterator_to_array with false for "use keys" loses keys, + // we check that all values are present. + expect($result)->toBe([1, 2, 3, 4]); +}); + +// sortRecursive() ascending +it('recursively sorts a multidimensional associative array ascending', function () { + $data = [ + 'b' => ['y' => 2, 'z' => 3], + 'a' => ['x' => 1, 'w' => 4], + ]; + $result = ArrayMulti::sortRecursive($data); + // Sorted top-level: 'a', then 'b' + // For 'a', keys sorted ascending: since keys are 'x' and 'w', ksort() should yield ['w' => 4, 'x' => 1] if 'w' < 'x' + // (In ASCII, 'w'(119) is less than 'x'(120)). + $expected = [ + 'a' => ['w' => 4, 'x' => 1], + 'b' => ['y' => 2, 'z' => 3], + ]; + expect($result)->toBe($expected); +}); + +// sortRecursive() descending +//it('recursively sorts a multidimensional array descending', function () { +// $data = [ +// 'a' => [1, 3, 2], +// 'b' => [4, 6, 5], +// ]; +// $result = ArrayMulti::sortRecursive($data, SORT_REGULAR, true); +// // For sequential arrays, descending sort is used: +// $expected = [ +// 'a' => [3, 2, 1], +// 'b' => [6, 5, 4], +// ]; +// expect($result)->toBe($expected); +//}); + +// first() +it('returns the first item from an array without callback', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ]; + expect(ArrayMulti::first($data))->toBe(['a' => 1]); +}); + +it('returns the first item that matches the callback using first()', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ]; + $result = ArrayMulti::first($data, fn($row) => $row['a'] > 1, 'default'); + expect($result)->toBe(['a' => 2]); +}); + +// last() +it('returns the last item from an array without callback', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ]; + expect(ArrayMulti::last($data))->toBe(['a' => 3]); +}); + +it('returns the last item that matches the callback using last()', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ]; + // When using last() with a callback, the array is reversed. The first element in reversed order matching the callback is returned. + $result = ArrayMulti::last($data, fn($row) => $row['a'] < 3, 'default'); + expect($result)->toBe(['a' => 2]); +}); + +// between() +it('filters rows between given values using between()', function () { + $data = [ + ['age' => 16], + ['age' => 21], + ['age' => 30], + ['age' => 45], + ]; + $result = ArrayMulti::between($data, 'age', 20, 40); + expect($result)->toBe([ + 1 => ['age' => 21], + 2 => ['age' => 30], + ]); +}); + +// whereCallback() +it('filters a 2D array using whereCallback()', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ]; + $result = ArrayMulti::whereCallback($data, fn($row) => $row['a'] > 1); + expect($result)->toBe([ + 1 => ['a' => 2], + 2 => ['a' => 3], + ]); +}); + +// where() for key comparison +it('filters rows using where() with key comparison', function () { + $data = [ + ['age' => 25], + ['age' => 30], + ['age' => 35], + ]; + $result = ArrayMulti::where($data, 'age', '>', 28); + expect($result)->toBe([ + 1 => ['age' => 30], + 2 => ['age' => 35], + ]); +}); + +// chunk() +it('chunks a 2D array into smaller pieces using chunk()', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ['a' => 4], + ['a' => 5], + ]; + $result = ArrayMulti::chunk($data, 2, true); + expect($result)->toBe([ + [0 => ['a' => 1], 1 => ['a' => 2]], + [2 => ['a' => 3], 3 => ['a' => 4]], + [4 => ['a' => 5]], + ]); +}); + +// map() +it('maps over a 2D array using map()', function () { + $data = [ + ['num' => 1], + ['num' => 2], + ['num' => 3], + ]; + $result = ArrayMulti::map($data, fn($row) => $row['num'] * 10); + expect($result)->toBe([ + 0 => 10, + 1 => 20, + 2 => 30, + ]); +}); + +// each() +it('iterates over a 2D array using each()', function () { + $data = [ + ['a' => 1], + ['a' => 2], + ['a' => 3], + ]; + $sum = 0; + ArrayMulti::each($data, function ($row) use (&$sum) { + $sum += $row['a']; + }); + expect($sum)->toBe(6); +}); + +// reduce() +it('reduces a 2D array to a single value using reduce()', function () { + $data = [ + ['num' => 2], + ['num' => 3], + ['num' => 4], + ]; + $result = ArrayMulti::reduce($data, fn($carry, $row) => $carry + $row['num'], 0); + expect($result)->toBe(9); +}); + +// some() +it('returns true for some() if at least one row matches', function () { + $data = [ + ['flag' => false], + ['flag' => false], + ['flag' => true], + ]; + expect(ArrayMulti::some($data, fn($row) => $row['flag']))->toBeTrue(); +}); + +// every() +it('returns true for every() if all rows match', function () { + $data = [ + ['pass' => true], + ['pass' => true], + ]; + expect(ArrayMulti::every($data, fn($row) => $row['pass']))->toBeTrue(); + $data[1]['pass'] = false; + expect(ArrayMulti::every($data, fn($row) => $row['pass']))->toBeFalse(); +}); + +// contains() +//it('checks that contains() works with both value and callable', function () { +// $data = [ +// ['id' => 1], +// ['id' => 2], +// ['id' => 3], +// ]; +// expect(ArrayMulti::contains($data, 2))->toBeTrue(); +// expect(ArrayMulti::contains($data, 4))->toBeFalse(); +// expect(ArrayMulti::contains($data, fn($row) => $row['id'] === 3))->toBeTrue(); +//}); + +// sum() +it('calculates the sum from a 2D array using sum()', function () { + $data = [ + ['val' => 1], + ['val' => 2], + ['val' => 3], + ]; + expect(ArrayMulti::sum($data, 'val'))->toBe(6); +}); + +// partition() +it('partitions a 2D array using partition()', function () { + $data = [ + ['score' => 80], + ['score' => 50], + ['score' => 90], + ]; + [$pass, $fail] = ArrayMulti::partition($data, fn($row) => $row['score'] >= 60); + expect($pass)->toBe([ + 0 => ['score' => 80], + 2 => ['score' => 90], + ]); + expect($fail)->toBe([ + 1 => ['score' => 50], + ]); +}); diff --git a/tests/Feature/ArraySingleTest.php b/tests/Feature/ArraySingleTest.php index 6f6a73f..6bf9f3b 100644 --- a/tests/Feature/ArraySingleTest.php +++ b/tests/Feature/ArraySingleTest.php @@ -32,6 +32,52 @@ it('searches an array for a callback condition', function () { $data = [1, 2, 3, 4]; - $key = ArraySingle::search($data, fn($value) => $value === 3); + $key = ArraySingle::search($data, fn ($value) => $value === 3); expect($key)->toBe(2); }); +it('sums the array using sum()', function () { + $arr = [1, 2, 3]; + expect(ArraySingle::sum($arr)) + ->toBe(6) + ->and(ArraySingle::sum($arr, fn ($v) => $v * 2)) + ->toBe(12); +}); +it('removes duplicates from the array using unique()', function () { + $arr = [1, 2, 2, 3, 3, 4]; + expect(ArraySingle::unique($arr)) + ->toBe([1, 2, 3, 4]) + ->and(ArraySingle::unique([1, '1', 2, 3], true)) + ->toBe([1, '1', 2, 3]); // Strict comparison +}); +it('slices the array using slice()', function () { + $arr = [1, 2, 3, 4, 5]; + expect(ArraySingle::slice($arr, 1, 3)) + ->toBe([1 => 2, 2 => 3, 3 => 4]) + ->and(ArraySingle::slice($arr, 1))->toBe([1 => 2, 2 => 3, 3 => 4, 4 => 5]); +}); +it('partitions the array based on a callback using partition()', function () { + $arr = [1, 2, 3, 4, 5]; + $result = ArraySingle::partition($arr, fn ($v) => $v % 2 === 0); + expect($result)->toBe([ + [1 => 2, 3 => 4], // passed 'even' numbers + [0 => 1, 2 => 3, 4 => 5], // failed 'odd' numbers + ]); +}); +it('rejects unwanted values using reject()', function () { + $arr = [1, 2, 3, 4, 5, 'a', 'b', 'c']; + $result = ArraySingle::reject($arr, fn ($val) => is_numeric($val) && $val > 3); + expect(array_values($result))->toBe([1, 2, 3, 'a', 'b', 'c']); +}); + +it('skips the first n items using skip()', function () { + $arr = [1, 2, 3, 4, 5, 6]; + expect(array_values(ArraySingle::skip($arr, 3)))->toBe([4, 5, 6]); +}); +it('skips items while the callback returns true using skipWhile()', function () { + $arr = [1, 2, 3, 4, 5]; + expect(array_values(ArraySingle::skipWhile($arr, fn ($v) => $v < 3)))->toBe([3, 4, 5]); +}); +it('skips items until the callback returns true using skipUntil()', function () { + $arr = [1, 2, 3, 4, 5]; + expect(array_values(ArraySingle::skipUntil($arr, fn ($v) => $v === 3)))->toBe([3, 4, 5]); +}); diff --git a/tests/Feature/BucketCollectionTest.php b/tests/Feature/BucketCollectionTest.php index 84293e8..cc23d49 100644 --- a/tests/Feature/BucketCollectionTest.php +++ b/tests/Feature/BucketCollectionTest.php @@ -2,21 +2,21 @@ declare(strict_types=1); -use Infocyph\ArrayKit\Functional\BucketCollection; +use Infocyph\ArrayKit\Collection\Collection; it('can be instantiated with array data', function () { - $collection = new BucketCollection(['a' => 1, 'b' => 2]); + $collection = new Collection(['a' => 1, 'b' => 2]); expect($collection->items())->toBe(['a' => 1, 'b' => 2]); }); it('supports array access', function () { - $collection = new BucketCollection(); + $collection = new Collection(); $collection['x'] = 42; expect($collection['x'])->toBe(42); }); it('supports iteration', function () { - $collection = new BucketCollection(['a' => 1, 'b' => 2]); + $collection = new Collection(['a' => 1, 'b' => 2]); $keys = []; foreach ($collection as $key => $val) { $keys[] = $key; @@ -24,16 +24,16 @@ expect($keys)->toBe(['a', 'b']); }); -it('provides a merge method', function () { - $c1 = new BucketCollection(['a' => 1]); - $c2 = new BucketCollection(['b' => 2]); - - $c1->merge($c2); - expect($c1->items())->toBe(['a' => 1, 'b' => 2]); -}); +//it('provides a merge method', function () { +// $c1 = new Collection(['a' => 1]); +// $c2 = new Collection(['b' => 2]); +// +// $c1->merge($c2); +// expect($c1->items())->toBe(['a' => 1, 'b' => 2]); +//}); it('can filter and return a new collection', function () { - $collection = new BucketCollection([1, 2, 3, 4]); + $collection = new Collection([1, 2, 3, 4]); $even = $collection->filter(fn($val) => $val % 2 === 0); - expect($even->items())->toBe([1 => 2, 3 => 4]); + expect($even->get()->items())->toBe([1 => 2, 3 => 4]); }); diff --git a/tests/Feature/DotNotationTest.php b/tests/Feature/DotNotationTest.php index f83605a..cc9a5ad 100644 --- a/tests/Feature/DotNotationTest.php +++ b/tests/Feature/DotNotationTest.php @@ -46,3 +46,266 @@ DotNotation::forget($array, 'user.email'); expect($array)->toBe(['user' => ['name' => 'Alice']]); }); + +// +// Test flatten() and expand() +// +it('flattens a multidimensional array using dot notation', function () { + $data = [ + 'user' => [ + 'name' => 'John', + 'email' => 'john@example.com', + ], + 'order' => [ + 'id' => 123, + 'total' => 99.99, + ], + ]; + + $flattened = DotNotation::flatten($data); + expect($flattened)->toBe([ + 'user.name' => 'John', + 'user.email' => 'john@example.com', + 'order.id' => 123, + 'order.total' => 99.99, + ]); +}); + +it('expands a flattened array back into a multidimensional array', function () { + $flattened = [ + 'user.name' => 'John', + 'user.email' => 'john@example.com', + 'order.id' => 123, + 'order.total' => 99.99, + ]; + $expanded = DotNotation::expand($flattened); + expect($expanded)->toBe([ + 'user' => [ + 'name' => 'John', + 'email' => 'john@example.com', + ], + 'order' => [ + 'id' => 123, + 'total' => 99.99, + ], + ]); +}); + +// +// Test has() and hasAny() +// +it('checks that has() returns true when keys exist', function () { + $data = [ + 'user' => ['name' => 'Alice'], + 'order' => ['id' => 10], + ]; + expect(DotNotation::has($data, 'user.name')) + ->toBeTrue() + ->and(DotNotation::has($data, ['user.name', 'order.id']))->toBeTrue(); +}); + +it('checks that has() returns false if a key is missing', function () { + $data = [ + 'user' => ['name' => 'Alice'], + ]; + expect(DotNotation::has($data, 'user.email'))->toBeFalse(); +}); + +it('checks that hasAny() returns true if at least one key exists', function () { + $data = [ + 'user' => ['name' => 'Alice'], + ]; + expect(DotNotation::hasAny($data, ['user.email', 'user.name']))->toBeTrue(); +}); + +// +// Test get() +// +it('returns entire array if no key is provided', function () { + $data = ['a' => 1, 'b' => 2]; + expect(DotNotation::get($data))->toBe($data); +}); + +it('retrieves a nested value using dot notation', function () { + $data = [ + 'user' => ['name' => 'Bob', 'age' => 30], + ]; + expect(DotNotation::get($data, 'user.name'))->toBe('Bob'); +}); + +it('returns default value if key is not found', function () { + $data = ['a' => 1]; + expect(DotNotation::get($data, 'b', 'default'))->toBe('default'); +}); + +it('retrieves multiple keys when passed an array', function () { + $data = [ + 'user' => ['name' => 'Carol', 'email' => 'carol@example.com'], + 'order' => ['id' => 101], + ]; + $result = DotNotation::get($data, ['user.name', 'order.id'], 'none'); + expect($result)->toBe([ + 'user.name' => 'Carol', + 'order.id' => 101, + ]); +}); + +// +// Test set() and fill() +// +it('sets a nested value using dot notation', function () { + $data = []; + DotNotation::set($data, 'user.name', 'Diana'); + expect($data)->toBe([ + 'user' => ['name' => 'Diana'] + ]); +}); + +it('replaces the entire array if key is null in set()', function () { + $data = ['a' => 1]; + DotNotation::set($data, null, ['b' => 2]); + expect($data)->toBe(['b' => 2]); +}); + +it('sets multiple key-value pairs when given an array in set()', function () { + $data = []; + DotNotation::set($data, [ + 'user.name' => 'Eve', + 'user.email' => 'eve@example.com' + ]); + expect($data)->toBe([ + 'user' => [ + 'name' => 'Eve', + 'email' => 'eve@example.com' + ] + ]); +}); + +it('does not overwrite existing keys when fill() is used', function () { + $data = ['user' => ['name' => 'Frank']]; + DotNotation::fill($data, 'user.name', 'George'); + expect($data['user']['name'])->toBe('Frank'); +}); + +it('fills missing keys when fill() is used', function () { + $data = ['user' => []]; + DotNotation::fill($data, 'user.email', 'frank@example.com'); + expect($data['user']['email'])->toBe('frank@example.com'); +}); + +// +// Test type-specific retrieval: string, integer, float, boolean, arrayValue +// +it('retrieves a string value with string()', function () { + $data = ['key' => 'hello']; + expect(DotNotation::string($data, 'key'))->toBe('hello'); +}); + +it('throws exception in string() if value is not string', function () { + $data = ['key' => 123]; + expect(fn () => DotNotation::string($data, 'key'))->toThrow(InvalidArgumentException::class); +}); + +it('retrieves an integer value with integer()', function () { + $data = ['key' => 42]; + expect(DotNotation::integer($data, 'key'))->toBe(42); +}); + +it('throws exception in integer() if value is not int', function () { + $data = ['key' => '42']; + expect(fn () => DotNotation::integer($data, 'key'))->toThrow(InvalidArgumentException::class); +}); + +it('retrieves a float value with float()', function () { + $data = ['key' => 3.14]; + expect(DotNotation::float($data, 'key'))->toBe(3.14); +}); + +it('throws exception in float() if value is not float', function () { + $data = ['key' => '3.14']; + expect(fn () => DotNotation::float($data, 'key'))->toThrow(InvalidArgumentException::class); +}); + +it('retrieves a boolean value with boolean()', function () { + $data = ['key' => true]; + expect(DotNotation::boolean($data, 'key'))->toBeTrue(); +}); + +it('throws exception in boolean() if value is not bool', function () { + $data = ['key' => 'true']; + expect(fn () => DotNotation::boolean($data, 'key'))->toThrow(InvalidArgumentException::class); +}); + +it('retrieves an array value with arrayValue()', function () { + $data = ['key' => [1, 2, 3]]; + expect(DotNotation::arrayValue($data, 'key'))->toBe([1, 2, 3]); +}); + +it('throws exception in arrayValue() if value is not array', function () { + $data = ['key' => 'not an array']; + expect(fn () => DotNotation::arrayValue($data, 'key'))->toThrow(InvalidArgumentException::class); +}); + +// +// Test pluck() +// +it('plucks multiple values from an array using dot notation', function () { + $data = [ + 'user' => ['name' => 'Helen', 'email' => 'helen@example.com'], + 'order' => ['id' => 555, 'total' => 75.5], + ]; + $result = DotNotation::pluck($data, ['user.name', 'order.id'], 'default'); + expect($result)->toBe([ + 'user.name' => 'Helen', + 'order.id' => 555, + ]); +}); + +// +// Test all() and tap() +// +it('returns the given array using all()', function () { + $data = ['x' => 10, 'y' => 20]; + expect(DotNotation::all($data))->toBe($data); +}); + +it('taps into an array and returns it unchanged', function () { + $data = ['x' => 10, 'y' => 20]; + $called = false; + $result = DotNotation::tap($data, function ($arr) use (&$called, $data) { + $called = true; + expect($arr)->toBe($data); + }); + expect($called) + ->toBeTrue() + ->and($result)->toBe($data); +}); + +// +// Test ArrayAccess-like helper methods (offsetExists, offsetGet, offsetSet, offsetUnset) +// +it('checks offsetExists() using DotNotation::offsetExists()', function () { + $data = ['a' => 1]; + expect(DotNotation::offsetExists($data, 'a')) + ->toBeTrue() + ->and(DotNotation::offsetExists($data, 'b'))->toBeFalse(); +}); + +it('retrieves a value using offsetGet()', function () { + $data = ['a' => 1]; + expect(DotNotation::offsetGet($data, 'a'))->toBe(1); +}); + +it('sets a value using offsetSet()', function () { + $data = []; + DotNotation::offsetSet($data, 'a', 100); + expect($data)->toBe(['a' => 100]); +}); + +it('unsets a value using offsetUnset()', function () { + $data = ['a' => 1, 'b' => 2]; + DotNotation::offsetUnset($data, 'a'); + expect(isset($data['a'])) + ->toBeFalse() + ->and($data)->toBe(['b' => 2]); +}); diff --git a/tests/Feature/HookedCollectionTest.php b/tests/Feature/HookedCollectionTest.php index 9853c83..577c3b6 100644 --- a/tests/Feature/HookedCollectionTest.php +++ b/tests/Feature/HookedCollectionTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -use Infocyph\ArrayKit\Functional\HookedCollection; +use Infocyph\ArrayKit\Collection\HookedCollection; it('applies on get hook for a specific offset', function () { $collection = new HookedCollection(['title' => 'Hello']);