diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php index 541415a5ab918..6f6dc736a5510 100644 --- a/ext/standard/basic_functions.stub.php +++ b/ext/standard/basic_functions.stub.php @@ -1629,6 +1629,12 @@ function min(mixed $value, mixed ...$values): mixed {} */ function max(mixed $value, mixed ...$values): mixed {} +/** + * @compile-time-eval + * @frameless-function {"arity": 3} + */ +function clamp(mixed $value, mixed $min, mixed $max): mixed {} + function array_walk(array|object &$array, callable $callback, mixed $arg = UNKNOWN): true {} function array_walk_recursive(array|object &$array, callable $callback, mixed $arg = UNKNOWN): true {} diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h index ba9d1710137cc..d568b6341a0ca 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: deb4ea96dd130d8a0174678095c30a61e118bd60 */ + * Stub hash: a5eef8f94b2c661c0fd50202e3bf1e01a282c24f */ 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) @@ -138,6 +138,12 @@ ZEND_END_ARG_INFO() #define arginfo_max arginfo_min +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_clamp, 0, 3, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, value, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, min, IS_MIXED, 0) + ZEND_ARG_TYPE_INFO(0, max, IS_MIXED, 0) +ZEND_END_ARG_INFO() + ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_array_walk, 0, 2, IS_TRUE, 0) ZEND_ARG_TYPE_MASK(1, array, MAY_BE_ARRAY|MAY_BE_OBJECT, NULL) ZEND_ARG_TYPE_INFO(0, callback, IS_CALLABLE, 0) @@ -2197,6 +2203,12 @@ static const zend_frameless_function_info frameless_function_infos_max[] = { { 0 }, }; +ZEND_FRAMELESS_FUNCTION(clamp, 3); +static const zend_frameless_function_info frameless_function_infos_clamp[] = { + { ZEND_FRAMELESS_FUNCTION_NAME(clamp, 3), 3 }, + { 0 }, +}; + ZEND_FRAMELESS_FUNCTION(in_array, 2); ZEND_FRAMELESS_FUNCTION(in_array, 3); static const zend_frameless_function_info frameless_function_infos_in_array[] = { @@ -2332,6 +2344,7 @@ ZEND_FUNCTION(current); ZEND_FUNCTION(key); ZEND_FUNCTION(min); ZEND_FUNCTION(max); +ZEND_FUNCTION(clamp); ZEND_FUNCTION(array_walk); ZEND_FUNCTION(array_walk_recursive); ZEND_FUNCTION(in_array); @@ -2925,6 +2938,7 @@ static const zend_function_entry ext_functions[] = { ZEND_FE(key, arginfo_key) ZEND_RAW_FENTRY("min", zif_min, arginfo_min, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_min, NULL) ZEND_RAW_FENTRY("max", zif_max, arginfo_max, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_max, NULL) + ZEND_RAW_FENTRY("clamp", zif_clamp, arginfo_clamp, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_clamp, NULL) ZEND_FE(array_walk, arginfo_array_walk) ZEND_FE(array_walk_recursive, arginfo_array_walk_recursive) ZEND_RAW_FENTRY("in_array", zif_in_array, arginfo_in_array, ZEND_ACC_COMPILE_TIME_EVAL, frameless_function_infos_in_array, NULL) diff --git a/ext/standard/math.c b/ext/standard/math.c index 142d473864f75..f2ba5083a5fcb 100644 --- a/ext/standard/math.c +++ b/ext/standard/math.c @@ -389,6 +389,79 @@ PHP_FUNCTION(round) } /* }}} */ +/* {{{ Return the given value if in range of min and max */ +PHP_FUNCTION(clamp) +{ + zval *zvalue, *zmin, *zmax; + + ZEND_PARSE_PARAMETERS_START(3, 3) + Z_PARAM_ZVAL(zvalue) + Z_PARAM_ZVAL(zmin) + Z_PARAM_ZVAL(zmax) + ZEND_PARSE_PARAMETERS_END(); + + if (EXPECTED(Z_TYPE_P(zmin) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmin)))) { + zend_argument_value_error(2, "cannot be NAN"); + RETURN_THROWS(); + } + + if (EXPECTED(Z_TYPE_P(zmax) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmax)))) { + zend_argument_value_error(3, "cannot be NAN"); + RETURN_THROWS(); + } + + if (zend_compare(zmin, zmax) > 0) { + zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)"); + RETURN_THROWS(); + } + + if (zend_compare(zmax, zvalue) == -1) { + RETURN_COPY(zmax); + } + + if (zend_compare(zvalue, zmin) == -1) { + RETURN_COPY(zmin); + } + + RETURN_COPY(zvalue); +} +/* }}} */ + +/* {{{ Return the given value if in range of min and max */ +ZEND_FRAMELESS_FUNCTION(clamp, 3) +{ + zval *zvalue, *zmin, *zmax; + Z_FLF_PARAM_ZVAL(1, zvalue); + Z_FLF_PARAM_ZVAL(2, zmin); + Z_FLF_PARAM_ZVAL(3, zmax); + + if (EXPECTED(Z_TYPE_P(zmin) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmin)))) { + zend_argument_value_error(2, "cannot be NAN"); + RETURN_THROWS(); + } + + if (EXPECTED(Z_TYPE_P(zmax) == IS_DOUBLE) && UNEXPECTED(zend_isnan(Z_DVAL_P(zmax)))) { + zend_argument_value_error(3, "cannot be NAN"); + RETURN_THROWS(); + } + + if (zend_compare(zmin, zmax) > 0) { + zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)"); + RETURN_THROWS(); + } + + if (zend_compare(zmax, zvalue) == -1) { + RETURN_COPY(zmax); + } + + if (zend_compare(zvalue, zmin) == -1) { + RETURN_COPY(zmin); + } + + RETURN_COPY(zvalue); +} +/* }}} */ + /* {{{ Returns the sine of the number in radians */ PHP_FUNCTION(sin) { diff --git a/ext/standard/tests/math/clamp.phpt b/ext/standard/tests/math/clamp.phpt new file mode 100644 index 0000000000000..bd8fdf9e09ad2 --- /dev/null +++ b/ext/standard/tests/math/clamp.phpt @@ -0,0 +1,61 @@ +--TEST-- +clamp() tests +--INI-- +precision=14 +date.timezone = UTC +--FILE-- +format('Y-m-d'), "\n"; +echo clamp(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n"; + +try { + var_dump(clamp(4, NAN, 6)); +} catch (ValueError $error) { + echo $error->getMessage(), "\n"; +} + +try { + var_dump(clamp(7, 6, NAN)); +} catch (ValueError $error) { + echo $error->getMessage(), "\n"; +} + +try { + var_dump(clamp(1, 3, 2)); +} catch (ValueError $error) { + echo $error->getMessage(), "\n"; +} +?> +--EXPECT-- +int(2) +int(1) +int(3) +int(2) +float(2.5) +float(2.5) +float(1.3) +float(3.141592653589793) +float(NAN) +string(1) "c" +string(1) "d" +2025-08-15 +2025-08-20 +2025-08-15 +2025-08-20 +clamp(): Argument #2 ($min) cannot be NAN +clamp(): Argument #3 ($max) cannot be NAN +clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)