1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

[RFC] Add clamp function (#19434)

* Implement clamp function

Co-authored-by: thinkverse <hallberg.kim@gmail.com>

* - Use a common function for normal and frameless implementations
- Add tests for null and not-comparable cases
- Fix object support for frameless clamp function
- Improve NAN handling

* Create tests triggering both frameless and dynamic variants

* Add changelog

* [Review] rephrase error messages to use "must not"

* Enable assert()

---------

Co-authored-by: thinkverse <hallberg.kim@gmail.com>
This commit is contained in:
Kyle
2025-12-18 20:27:30 +01:00
committed by GitHub
parent 833120eb72
commit 06ea0e5df1
6 changed files with 186 additions and 1 deletions

1
NEWS
View File

@@ -7,6 +7,7 @@ PHP NEWS
request. (ilutov)
. It is now possible to use reference assign on WeakMap without the key
needing to be present beforehand. (ndossche)
. Added `clamp()`. (kylekatarnls, thinkverse)
- Hash:
. Upgrade xxHash to 0.8.2. (timwolla)

View File

@@ -71,6 +71,10 @@ PHP 8.6 UPGRADE NOTES
6. New Functions
========================================
- Standard:
. `clamp()` returns the given value if in range, else return the nearest bound.
RFC: https://wiki.php.net/rfc/clamp_v2
========================================
7. New Classes and Interfaces
========================================

View File

@@ -1606,6 +1606,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 {}

View File

@@ -1,5 +1,5 @@
/* This is a generated file, edit the .stub.php file instead.
* Stub hash: f6bf6cdd07080c01d3a0cb08d71409d05b1084f9 */
* Stub hash: 1a1667a5c59111f096a758d5bb4aa7cf3ec09cfe */
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)

View File

@@ -389,6 +389,62 @@ PHP_FUNCTION(round)
}
/* }}} */
/* Return the given value if in range of min and max */
static void php_math_clamp(zval *return_value, zval *value, zval *min, zval *max)
{
if (Z_TYPE_P(min) == IS_DOUBLE && UNEXPECTED(zend_isnan(Z_DVAL_P(min)))) {
zend_argument_value_error(2, "must not be NAN");
RETURN_THROWS();
}
if (Z_TYPE_P(max) == IS_DOUBLE && UNEXPECTED(zend_isnan(Z_DVAL_P(max)))) {
zend_argument_value_error(3, "must not be NAN");
RETURN_THROWS();
}
if (zend_compare(max, min) == -1) {
zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)");
RETURN_THROWS();
}
if (zend_compare(max, value) == -1) {
RETURN_COPY(max);
}
if (zend_compare(value, min) == -1) {
RETURN_COPY(min);
}
RETURN_COPY(value);
}
/* {{{ 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();
php_math_clamp(return_value, zvalue, zmin, zmax);
}
/* }}} */
/* {{{ 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);
php_math_clamp(return_value, zvalue, zmin, zmax);
}
/* }}} */
/* {{{ Returns the sine of the number in radians */
PHP_FUNCTION(sin)
{

View File

@@ -0,0 +1,104 @@
--TEST--
clamp() tests
--INI--
precision=14
date.timezone=UTC
zend.assertions=1
--FILE--
<?php
function make_clamp_fcc() {
return clamp(...);
}
function check_clamp_result($value, $min, $max) {
$flf = clamp($value, $min, $max);
$dyn = make_clamp_fcc()($value, $min, $max);
assert($flf === $dyn || (is_nan($flf) && is_nan($dyn)));
return $flf;
}
function check_clamp_exception($value, $min, $max) {
try {
var_dump(clamp($value, $min, $max));
} catch (ValueError $error) {
echo $error->getMessage(), "\n";
}
try {
var_dump(make_clamp_fcc()($value, $min, $max));
} catch (ValueError $error) {
echo $error->getMessage(), "\n";
}
}
var_dump(check_clamp_result(2, 1, 3));
var_dump(check_clamp_result(0, 1, 3));
var_dump(check_clamp_result(6, 1, 3));
var_dump(check_clamp_result(2, 1.3, 3.4));
var_dump(check_clamp_result(2.5, 1, 3));
var_dump(check_clamp_result(2.5, 1.3, 3.4));
var_dump(check_clamp_result(0, 1.3, 3.4));
var_dump(check_clamp_result(M_PI, -INF, INF));
var_dump(check_clamp_result(NAN, 4, 6));
var_dump(check_clamp_result("a", "c", "g"));
var_dump(check_clamp_result("d", "c", "g"));
echo check_clamp_result('2025-08-01', '2025-08-15', '2025-09-15'), "\n";
echo check_clamp_result('2025-08-20', '2025-08-15', '2025-09-15'), "\n";
echo check_clamp_result(new \DateTimeImmutable('2025-08-01'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n";
echo check_clamp_result(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n";
var_dump(check_clamp_result(null, -1, 1));
var_dump(check_clamp_result(null, 1, 3));
var_dump(check_clamp_result(null, -3, -1));
var_dump(check_clamp_result(-9999, null, 10));
var_dump(check_clamp_result(12, null, 10));
$a = new \InvalidArgumentException('a');
$b = new \RuntimeException('b');
$c = new \LogicException('c');
echo check_clamp_result($a, $b, $c)::class, "\n";
echo check_clamp_result($b, $a, $c)::class, "\n";
echo check_clamp_result($c, $a, $b)::class, "\n";
check_clamp_exception(4, NAN, 6);
check_clamp_exception(7, 6, NAN);
check_clamp_exception(1, 3, 2);
check_clamp_exception(-9999, 5, null);
check_clamp_exception(12, -5, null);
?>
--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
int(-1)
int(1)
int(-3)
int(-9999)
int(10)
InvalidArgumentException
RuntimeException
LogicException
clamp(): Argument #2 ($min) must not be NAN
clamp(): Argument #2 ($min) must not be NAN
clamp(): Argument #3 ($max) must not be NAN
clamp(): Argument #3 ($max) must not be NAN
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)