Commit 06ea0e5df1d for php.net
commit 06ea0e5df1d63f1dbd7bf93295f2ff64d1855075
Author: Kyle <kylekatarnls@users.noreply.github.com>
Date: Thu Dec 18 20:27:30 2025 +0100
[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>
diff --git a/NEWS b/NEWS
index 9fa63137d88..cea609f1d72 100644
--- a/NEWS
+++ b/NEWS
@@ -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)
diff --git a/UPGRADING b/UPGRADING
index 4e511d09479..9a2159a33e7 100644
--- a/UPGRADING
+++ b/UPGRADING
@@ -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
========================================
diff --git a/ext/standard/basic_functions.stub.php b/ext/standard/basic_functions.stub.php
index 7913ca0e001..e27dca069c5 100644
--- a/ext/standard/basic_functions.stub.php
+++ b/ext/standard/basic_functions.stub.php
@@ -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 {}
diff --git a/ext/standard/basic_functions_arginfo.h b/ext/standard/basic_functions_arginfo.h
index 0a21d7d7642..6f202c01463 100644
Binary files a/ext/standard/basic_functions_arginfo.h and b/ext/standard/basic_functions_arginfo.h differ
diff --git a/ext/standard/math.c b/ext/standard/math.c
index 142d473864f..95384c06588 100644
--- a/ext/standard/math.c
+++ b/ext/standard/math.c
@@ -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)
{
diff --git a/ext/standard/tests/math/clamp.phpt b/ext/standard/tests/math/clamp.phpt
new file mode 100644
index 00000000000..beb4c8d5314
--- /dev/null
+++ b/ext/standard/tests/math/clamp.phpt
@@ -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)