From e4a8d5b16fe6f6f6f910173dde42119bd41ca299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joshua=20R=C3=BCsweg?= Date: Fri, 31 May 2024 19:39:12 +0200 Subject: [PATCH] RFC: array_find (#14108) see https://wiki.php.net/rfc/array_find --- NEWS | 1 + UPGRADING | 3 + ext/standard/array.c | 159 ++++++++++++++ ext/standard/basic_functions.stub.php | 8 + ext/standard/basic_functions_arginfo.h | 24 +- ext/standard/tests/array/array_all_basic.phpt | 86 ++++++++ ext/standard/tests/array/array_any_basic.phpt | 91 ++++++++ .../tests/array/array_find_basic.phpt | 90 ++++++++ .../tests/array/array_find_key_basic.phpt | 91 ++++++++ .../tests/array/array_find_types.phpt | 205 ++++++++++++++++++ 10 files changed, 757 insertions(+), 1 deletion(-) create mode 100644 ext/standard/tests/array/array_all_basic.phpt create mode 100644 ext/standard/tests/array/array_any_basic.phpt create mode 100644 ext/standard/tests/array/array_find_basic.phpt create mode 100644 ext/standard/tests/array/array_find_key_basic.phpt create mode 100644 ext/standard/tests/array/array_find_types.phpt diff --git a/NEWS b/NEWS index c1e42a20c14..9bad757d78f 100644 --- a/NEWS +++ b/NEWS @@ -283,6 +283,7 @@ PHP NEWS as the magic $http_response_header variable. . Add php_base64_encode_ex() API. (Remi) . Implemented "Raising zero to the power of negative number" RFC. (Jorg Sowa) + . Added array_find(), array_find_key(), array_all(), and array_any(). (josh) - XML: . Added XML_OPTION_PARSE_HUGE parser option. (nielsdos) diff --git a/UPGRADING b/UPGRADING index b2135163bb9..77237e760d2 100644 --- a/UPGRADING +++ b/UPGRADING @@ -566,6 +566,9 @@ PHP 8.4 UPGRADE NOTES http_clear_last_response_headers() that allows retrieving the same content as the magic $http_response_header variable. . Added function fpow() following rules of IEEE 754. + . Added functions array_find(), array_find_key(), array_all(), and + array_any(). + RFC: https://wiki.php.net/rfc/array_find - XSL: . Added XSLTProcessor::registerPhpFunctionNS(). diff --git a/ext/standard/array.c b/ext/standard/array.c index 592d25c2115..4ad175c8ebe 100644 --- a/ext/standard/array.c +++ b/ext/standard/array.c @@ -6580,6 +6580,165 @@ PHP_FUNCTION(array_filter) } /* }}} */ +/* {{{ Internal function to find an array element for a user closure. */ +static zend_result php_array_find(const HashTable *array, zend_fcall_info fci, zend_fcall_info_cache fci_cache, zval *result_key, zval *result_value, bool negate_condition) +{ + zend_ulong num_key; + zend_string *str_key; + zval retval; + zval args[2]; + zval *operand; + + if (result_value != NULL) { + ZVAL_UNDEF(result_value); + } + + if (result_key != NULL) { + ZVAL_UNDEF(result_key); + } + + if (zend_hash_num_elements(array) == 0) { + return SUCCESS; + } + + ZEND_ASSERT(ZEND_FCI_INITIALIZED(fci)); + + fci.retval = &retval; + fci.param_count = 2; + fci.params = args; + + ZEND_HASH_FOREACH_KEY_VAL(array, num_key, str_key, operand) { + /* Set up the key */ + if (!str_key) { + ZVAL_LONG(&args[1], num_key); + } else { + ZVAL_STR_COPY(&args[1], str_key); + } + + ZVAL_COPY(&args[0], operand); + + zend_result result = zend_call_function(&fci, &fci_cache); + if (EXPECTED(result == SUCCESS)) { + int retval_true; + + retval_true = zend_is_true(&retval); + zval_ptr_dtor(&retval); + + /* This negates the condition, if negate_condition is true. Otherwise it does nothing with `retval_true`. */ + retval_true ^= negate_condition; + + if (retval_true) { + if (result_value != NULL) { + ZVAL_COPY(result_value, &args[0]); + } + + if (result_key != NULL) { + ZVAL_COPY(result_key, &args[1]); + } + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + + return SUCCESS; + } + } + + zval_ptr_dtor(&args[0]); + zval_ptr_dtor(&args[1]); + + if (UNEXPECTED(result != SUCCESS)) { + return FAILURE; + } + } ZEND_HASH_FOREACH_END(); + + return SUCCESS; +} +/* }}} */ + +/* {{{ Search within an array and returns the first found element value. */ +PHP_FUNCTION(array_find) +{ + zval *array = NULL; + zend_fcall_info fci; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(array) + Z_PARAM_FUNC(fci, fci_cache) + ZEND_PARSE_PARAMETERS_END(); + + if (php_array_find(Z_ARR_P(array), fci, fci_cache, NULL, return_value, false) != SUCCESS) { + RETURN_THROWS(); + } + + if (Z_TYPE_P(return_value) == IS_UNDEF) { + RETURN_NULL(); + } +} +/* }}} */ + +/* {{{ Search within an array and returns the first found element key. */ +PHP_FUNCTION(array_find_key) +{ + zval *array = NULL; + zend_fcall_info fci; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(array) + Z_PARAM_FUNC(fci, fci_cache) + ZEND_PARSE_PARAMETERS_END(); + + if (php_array_find(Z_ARR_P(array), fci, fci_cache, return_value, NULL, false) != SUCCESS) { + RETURN_THROWS(); + } + + if (Z_TYPE_P(return_value) == IS_UNDEF) { + RETURN_NULL(); + } +} +/* }}} */ + +/* {{{ Search within an array and returns true if an element is found. */ +PHP_FUNCTION(array_any) +{ + zval *array = NULL; + zend_fcall_info fci; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(array) + Z_PARAM_FUNC(fci, fci_cache) + ZEND_PARSE_PARAMETERS_END(); + + if (php_array_find(Z_ARR_P(array), fci, fci_cache, return_value, NULL, false) != SUCCESS) { + RETURN_THROWS(); + } + + RETURN_BOOL(Z_TYPE_P(return_value) != IS_UNDEF); +} +/* }}} */ + +/* {{{ Search within an array and returns true if an element is found. */ +PHP_FUNCTION(array_all) +{ + zval *array = NULL; + zend_fcall_info fci; + zend_fcall_info_cache fci_cache = empty_fcall_info_cache; + + ZEND_PARSE_PARAMETERS_START(2, 2) + Z_PARAM_ARRAY(array) + Z_PARAM_FUNC(fci, fci_cache) + ZEND_PARSE_PARAMETERS_END(); + + if (php_array_find(Z_ARR_P(array), fci, fci_cache, return_value, NULL, true) != SUCCESS) { + RETURN_THROWS(); + } + + RETURN_BOOL(Z_TYPE_P(return_value) == IS_UNDEF); +} +/* }}} */ + /* {{{ Applies the callback to the elements in given arrays. */ PHP_FUNCTION(array_map) { diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index dd1f59c64c0..2e89f446d46 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1897,6 +1897,14 @@ function array_reduce(array $array, callable $callback, mixed $initial = null): function array_filter(array $array, ?callable $callback = null, int $mode = 0): array {} +function array_find(array $array, callable $callback): mixed {} + +function array_find_key(array $array, callable $callback): mixed {} + +function array_any(array $array, callable $callback): bool {} + +function array_all(array $array, callable $callback): bool {} + function array_map(?callable $callback, array $array, array ...$arrays): array {} /** diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index b7071f59707..f3d04ad2544 100644 --- a/ext/standard/basic_functions_arginfo.h +++ b/ext/standard/basic_functions_arginfo.h @@ -1,5 +1,5 @@ /* This is a generated file, edit the .stub.php file instead. - * Stub hash: 5d8e13990ce18bebc9c7e6a0a9a7ad8b7593d35b */ + * Stub hash: 7d588414e84ed62088cd5b1c668b29c0e51d406f */ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_set_time_limit, 0, 1, _IS_BOOL, 0) ZEND_ARG_TYPE_INFO(0, seconds, IS_LONG, 0) @@ -336,6 +336,20 @@ ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_filter, 0, 1, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO_WITH_DEFAULT_VALUE(0, mode, IS_LONG, 0, "0") ZEND_END_ARG_INFO() +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_find, 0, 2, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) +ZEND_END_ARG_INFO() + +#define arginfo_array_find_key arginfo_array_find + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_any, 0, 2, _IS_BOOL, 0) + ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) + ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) +ZEND_END_ARG_INFO() + +#define arginfo_array_all arginfo_array_any + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_map, 0, 2, IS_ARRAY, 0) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 1) ZEND_ARG_TYPE_INFO(0, array, IS_ARRAY, 0) @@ -2405,6 +2419,10 @@ ZEND_FUNCTION(array_sum); ZEND_FUNCTION(array_product); ZEND_FUNCTION(array_reduce); ZEND_FUNCTION(array_filter); +ZEND_FUNCTION(array_find); +ZEND_FUNCTION(array_find_key); +ZEND_FUNCTION(array_any); +ZEND_FUNCTION(array_all); ZEND_FUNCTION(array_map); ZEND_FUNCTION(array_key_exists); ZEND_FUNCTION(array_chunk); @@ -3030,6 +3048,10 @@ static const zend_function_entry ext_functions[] = { ZEND_RAW_FENTRY("array_product", zif_array_product, arginfo_array_product, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_FE(array_reduce, arginfo_array_reduce) ZEND_FE(array_filter, arginfo_array_filter) + ZEND_FE(array_find, arginfo_array_find) + ZEND_FE(array_find_key, arginfo_array_find_key) + ZEND_FE(array_any, arginfo_array_any) + ZEND_FE(array_all, arginfo_array_all) ZEND_FE(array_map, arginfo_array_map) ZEND_RAW_FENTRY("array_key_exists", zif_array_key_exists, arginfo_array_key_exists, ZEND_ACC_COMPILE_TIME_EVAL, NULL, NULL) ZEND_RAW_FENTRY("key_exists", zif_array_key_exists, arginfo_key_exists, 0, NULL, NULL) diff --git a/ext/standard/tests/array/array_all_basic.phpt b/ext/standard/tests/array/array_all_basic.phpt new file mode 100644 index 00000000000..84478519838 --- /dev/null +++ b/ext/standard/tests/array/array_all_basic.phpt @@ -0,0 +1,86 @@ +--TEST-- +Test array_all() function : basic functionality +--FILE-- + 1, + "b" => 2, + "c" => 3, + "d" => 4, + "e" => 5, +]; + +$array2 = [ + 1, 2, 3, 4, 5 +]; + +function even($input) { + return $input % 2 === 0; +} + +class SmallerTenClass { + public static function smallerTen($input) { + return $input < 10; + } +} + +var_dump(array_all($array1, fn($value) => $value > 0)); +var_dump(array_all($array2, fn($value) => $value > 0)); +var_dump(array_all($array2, fn($value) => $value > 1)); +var_dump(array_all([], fn($value) => true)); + +echo '*** Test Exception after false result ***' . PHP_EOL; +try { + var_dump(array_all($array2, function ($value) { + if ($value > 1) { + throw new Exception("Test-Exception"); + } + + return false; + })); +} catch (Exception) { + var_dump("Unexpected Exception"); +} + +echo '*** Test aborting with exception ***' . PHP_EOL; +try { + var_dump(array_all($array2, function ($value) { + if ($value === 2) { + throw new Exception("Test-Exception"); + } + + var_dump($value); + + return true; + })); +} catch (Exception) { + var_dump("Catched Exception"); +} + +var_dump(array_all($array1, 'even')); + +var_dump(array_all($array1, function($value) { + // return nothing +})); + +var_dump(array_all($array1, [ + 'SmallerTenClass', + 'smallerTen' +])); + +var_dump(array_all($array1, "SmallerTenClass::smallerTen")); +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(true) +*** Test Exception after false result *** +bool(false) +*** Test aborting with exception *** +int(1) +string(17) "Catched Exception" +bool(false) +bool(false) +bool(true) +bool(true) diff --git a/ext/standard/tests/array/array_any_basic.phpt b/ext/standard/tests/array/array_any_basic.phpt new file mode 100644 index 00000000000..59dffc1600c --- /dev/null +++ b/ext/standard/tests/array/array_any_basic.phpt @@ -0,0 +1,91 @@ +--TEST-- +Test array_any() function : basic functionality +--FILE-- + 1, + "b" => 2, + "c" => 3, + "d" => 4, + "e" => 5, +]; + +$array2 = [ + 1, 2, 3, 4, 5 +]; + +function even($input) { + return $input % 2 === 0; +} + +class EvenClass { + public static function even($input) { + return $input % 2 === 0; + } +} + + +var_dump(array_any($array1, fn($value) => $value > 3)); +var_dump(array_any($array2, fn($value) => $value > 3)); +var_dump(array_any($array2, fn($value) => $value > 5)); +var_dump(array_any([], fn($value) => true)); +var_dump(array_any($array1, fn($value, $key) => $key === "c")); +var_dump(array_any($array1, fn($value, $key) => false)); + +echo '*** Test Exception after true result ***' . PHP_EOL; +try { + var_dump(array_any($array2, function ($value) { + if ($value > 1) { + throw new Exception("Test-Exception"); + } + + return true; + })); +} catch (Exception) { + var_dump("Unexpected Exception"); +} + +echo '*** Test aborting with exception ***' . PHP_EOL; +try { + var_dump(array_any($array2, function ($value) { + if ($value === 2) { + throw new Exception("Test-Exception"); + } + + var_dump($value); + + return false; + })); +} catch (Exception) { + var_dump("Catched Exception"); +} + +var_dump(array_any($array1, 'even')); + +var_dump(array_any($array1, function($value) { + // return nothing +})); + +var_dump(array_any($array1, [ + 'EvenClass', + 'even' +])); + +var_dump(array_any($array1, "EvenClass::even")); +?> +--EXPECT-- +bool(true) +bool(true) +bool(false) +bool(false) +bool(true) +bool(false) +*** Test Exception after true result *** +bool(true) +*** Test aborting with exception *** +int(1) +string(17) "Catched Exception" +bool(true) +bool(false) +bool(true) +bool(true) diff --git a/ext/standard/tests/array/array_find_basic.phpt b/ext/standard/tests/array/array_find_basic.phpt new file mode 100644 index 00000000000..02964e98645 --- /dev/null +++ b/ext/standard/tests/array/array_find_basic.phpt @@ -0,0 +1,90 @@ +--TEST-- +Test array_find() function : basic functionality +--FILE-- + 1, + "b" => 2, + "c" => 3, + "d" => 4, + "e" => 5, +]; + +$array2 = [ + 1, 2, 3, 4, 5 +]; + +function even($input) { + return $input % 2 === 0; +} + +class EvenClass { + public static function even($input) { + return $input % 2 === 0; + } +} + +var_dump(array_find($array1, fn($value) => $value > 3)); +var_dump(array_find($array2, fn($value) => $value > 3)); +var_dump(array_find($array2, fn($value) => $value > 5)); +var_dump(array_find([], fn($value) => true)); +var_dump(array_find($array1, fn($value, $key) => $key === "c")); +var_dump(array_find($array1, fn($value, $key) => false)); + +echo '*** Test Exception after found result ***' . PHP_EOL; +try { + var_dump(array_find($array1, function ($value) { + if ($value > 1) { + throw new Exception("Test-Exception"); + } + + return true; + })); +} catch (Exception) { + var_dump("Unexpected Exception"); +} + +echo '*** Test aborting with exception ***' . PHP_EOL; +try { + var_dump(array_find($array2, function ($value) { + if ($value === 2) { + throw new Exception("Test-Exception"); + } + + var_dump($value); + + return false; + })); +} catch (Exception) { + var_dump("Catched Exception"); +} + +var_dump(array_find($array1, 'even')); + +var_dump(array_find($array1, function($value) { + // return nothing +})); + +var_dump(array_find($array1, [ + 'EvenClass', + 'even' +])); + +var_dump(array_find($array1, "EvenClass::even")); +?> +--EXPECT-- +int(4) +int(4) +NULL +NULL +int(3) +NULL +*** Test Exception after found result *** +int(1) +*** Test aborting with exception *** +int(1) +string(17) "Catched Exception" +int(2) +NULL +int(2) +int(2) diff --git a/ext/standard/tests/array/array_find_key_basic.phpt b/ext/standard/tests/array/array_find_key_basic.phpt new file mode 100644 index 00000000000..681880b0dc5 --- /dev/null +++ b/ext/standard/tests/array/array_find_key_basic.phpt @@ -0,0 +1,91 @@ +--TEST-- +Test array_find_key() function : basic functionality +--FILE-- + 1, + "b" => 2, + "c" => 3, + "d" => 4, + "e" => 5, +]; + +$array2 = [ + 1, 2, 3, 4, 5 +]; + +function even($input) { + return $input % 2 === 0; +} + +class EvenClass { + public static function even($input) { + return $input % 2 === 0; + } +} + + +var_dump(array_find_key($array1, fn($value) => $value > 3)); +var_dump(array_find_key($array2, fn($value) => $value > 3)); +var_dump(array_find_key($array2, fn($value) => $value > 5)); +var_dump(array_find_key([], fn($value) => true)); +var_dump(array_find_key($array1, fn($value, $key) => $key === "c")); +var_dump(array_find_key($array1, fn($value, $key) => false)); + +echo '*** Test Exception after found result ***' . PHP_EOL; +try { + var_dump(array_find_key($array1, function ($value) { + if ($value > 1) { + throw new Exception("Test-Exception"); + } + + return true; + })); +} catch (Exception) { + var_dump("Unexpected Exception"); +} + +echo '*** Test aborting with exception ***' . PHP_EOL; +try { + var_dump(array_find_key($array2, function ($value) { + if ($value === 2) { + throw new Exception("Test-Exception"); + } + + var_dump($value); + + return false; + })); +} catch (Exception) { + var_dump("Catched Exception"); +} + +var_dump(array_find_key($array1, 'even')); + +var_dump(array_find_key($array1, function($value) { + // return nothing +})); + +var_dump(array_find_key($array1, [ + 'EvenClass', + 'even' +])); + +var_dump(array_find_key($array1, "EvenClass::even")); +?> +--EXPECT-- +string(1) "d" +int(3) +NULL +NULL +string(1) "c" +NULL +*** Test Exception after found result *** +string(1) "a" +*** Test aborting with exception *** +int(1) +string(17) "Catched Exception" +string(1) "b" +NULL +string(1) "b" +string(1) "b" diff --git a/ext/standard/tests/array/array_find_types.phpt b/ext/standard/tests/array/array_find_types.phpt new file mode 100644 index 00000000000..71cee70be8b --- /dev/null +++ b/ext/standard/tests/array/array_find_types.phpt @@ -0,0 +1,205 @@ +--TEST-- +basic array_find test +--FILE-- +a = "1"; + +$array = [ + ...[0, 1, 2, -1, 034, 0X4A], + ...[0.0, 1.2, 1.2e3, 1.2e-3], + ...['value1', "value2", '', " ", ""], + ...[true, false, TRUE, FALSE], + ...[null, NULL], + ...[1 => 'one', 'zero' => 0, -2 => "value"], + ...["one" => 1, 5 => "float", true => 1, "" => 'empty'], + ...[1 => 'one', 2, "key" => 'value'], + ...[new stdClass(), $stdClass, (object)['b' => 2]], + ...[[], [1, 2, 3], [-1 => 1, -2 => 2]], + ...[@$undefined_var] +]; + +echo "*** Dumping each key value pair. ***". PHP_EOL; +var_dump(array_find($array, function ($value, $key) { + var_dump($key, $value); + + return false; +})); + +echo "*** Dumping only first key value pair. ***". PHP_EOL; +var_dump(array_find($array, function ($value, $key) { + var_dump($key, $value); + + return true; +})); + +echo "*** Returning each value. ***". PHP_EOL; +foreach (array_keys($array) as $key) { + var_dump(array_find($array, function ($value, $arrayKey) use ($key) { + return $arrayKey === $key; + })); +} +?> +--EXPECT-- +*** Dumping each key value pair. *** +int(0) +int(0) +int(1) +int(1) +int(2) +int(2) +int(3) +int(-1) +int(4) +int(28) +int(5) +int(74) +int(6) +float(0) +int(7) +float(1.2) +int(8) +float(1200) +int(9) +float(0.0012) +int(10) +string(6) "value1" +int(11) +string(6) "value2" +int(12) +string(0) "" +int(13) +string(1) " " +int(14) +string(0) "" +int(15) +bool(true) +int(16) +bool(false) +int(17) +bool(true) +int(18) +bool(false) +int(19) +NULL +int(20) +NULL +int(21) +string(3) "one" +string(4) "zero" +int(0) +int(22) +string(5) "value" +string(3) "one" +int(1) +int(23) +string(5) "float" +int(24) +int(1) +string(0) "" +string(5) "empty" +int(25) +string(3) "one" +int(26) +int(2) +string(3) "key" +string(5) "value" +int(27) +object(stdClass)#2 (0) { +} +int(28) +object(stdClass)#1 (1) { + ["a"]=> + string(1) "1" +} +int(29) +object(stdClass)#3 (1) { + ["b"]=> + int(2) +} +int(30) +array(0) { +} +int(31) +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +int(32) +array(2) { + [-1]=> + int(1) + [-2]=> + int(2) +} +int(33) +NULL +NULL +*** Dumping only first key value pair. *** +int(0) +int(0) +int(0) +*** Returning each value. *** +int(0) +int(1) +int(2) +int(-1) +int(28) +int(74) +float(0) +float(1.2) +float(1200) +float(0.0012) +string(6) "value1" +string(6) "value2" +string(0) "" +string(1) " " +string(0) "" +bool(true) +bool(false) +bool(true) +bool(false) +NULL +NULL +string(3) "one" +int(0) +string(5) "value" +int(1) +string(5) "float" +int(1) +string(5) "empty" +string(3) "one" +int(2) +string(5) "value" +object(stdClass)#2 (0) { +} +object(stdClass)#1 (1) { + ["a"]=> + string(1) "1" +} +object(stdClass)#3 (1) { + ["b"]=> + int(2) +} +array(0) { +} +array(3) { + [0]=> + int(1) + [1]=> + int(2) + [2]=> + int(3) +} +array(2) { + [-1]=> + int(1) + [-2]=> + int(2) +} +NULL