Skip to content

Commit 06ea0e5

Browse files
[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>
1 parent 833120e commit 06ea0e5

File tree

6 files changed

+186
-1
lines changed

6 files changed

+186
-1
lines changed

NEWS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ PHP NEWS
77
request. (ilutov)
88
. It is now possible to use reference assign on WeakMap without the key
99
needing to be present beforehand. (ndossche)
10+
. Added `clamp()`. (kylekatarnls, thinkverse)
1011

1112
- Hash:
1213
. Upgrade xxHash to 0.8.2. (timwolla)

UPGRADING

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,10 @@ PHP 8.6 UPGRADE NOTES
7171
6. New Functions
7272
========================================
7373

74+
- Standard:
75+
. `clamp()` returns the given value if in range, else return the nearest bound.
76+
RFC: https://wiki.php.net/rfc/clamp_v2
77+
7478
========================================
7579
7. New Classes and Interfaces
7680
========================================

ext/standard/basic_functions.stub.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,6 +1606,12 @@ function min(mixed $value, mixed ...$values): mixed {}
16061606
*/
16071607
function max(mixed $value, mixed ...$values): mixed {}
16081608

1609+
/**
1610+
* @compile-time-eval
1611+
* @frameless-function {"arity": 3}
1612+
*/
1613+
function clamp(mixed $value, mixed $min, mixed $max): mixed {}
1614+
16091615
function array_walk(array|object &$array, callable $callback, mixed $arg = UNKNOWN): true {}
16101616

16111617
function array_walk_recursive(array|object &$array, callable $callback, mixed $arg = UNKNOWN): true {}

ext/standard/basic_functions_arginfo.h

Lines changed: 15 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ext/standard/math.c

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,62 @@ PHP_FUNCTION(round)
389389
}
390390
/* }}} */
391391

392+
/* Return the given value if in range of min and max */
393+
static void php_math_clamp(zval *return_value, zval *value, zval *min, zval *max)
394+
{
395+
if (Z_TYPE_P(min) == IS_DOUBLE && UNEXPECTED(zend_isnan(Z_DVAL_P(min)))) {
396+
zend_argument_value_error(2, "must not be NAN");
397+
RETURN_THROWS();
398+
}
399+
400+
if (Z_TYPE_P(max) == IS_DOUBLE && UNEXPECTED(zend_isnan(Z_DVAL_P(max)))) {
401+
zend_argument_value_error(3, "must not be NAN");
402+
RETURN_THROWS();
403+
}
404+
405+
if (zend_compare(max, min) == -1) {
406+
zend_argument_value_error(2, "must be smaller than or equal to argument #3 ($max)");
407+
RETURN_THROWS();
408+
}
409+
410+
if (zend_compare(max, value) == -1) {
411+
RETURN_COPY(max);
412+
}
413+
414+
if (zend_compare(value, min) == -1) {
415+
RETURN_COPY(min);
416+
}
417+
418+
RETURN_COPY(value);
419+
}
420+
421+
/* {{{ Return the given value if in range of min and max */
422+
PHP_FUNCTION(clamp)
423+
{
424+
zval *zvalue, *zmin, *zmax;
425+
426+
ZEND_PARSE_PARAMETERS_START(3, 3)
427+
Z_PARAM_ZVAL(zvalue)
428+
Z_PARAM_ZVAL(zmin)
429+
Z_PARAM_ZVAL(zmax)
430+
ZEND_PARSE_PARAMETERS_END();
431+
432+
php_math_clamp(return_value, zvalue, zmin, zmax);
433+
}
434+
/* }}} */
435+
436+
/* {{{ Return the given value if in range of min and max */
437+
ZEND_FRAMELESS_FUNCTION(clamp, 3)
438+
{
439+
zval *zvalue, *zmin, *zmax;
440+
Z_FLF_PARAM_ZVAL(1, zvalue);
441+
Z_FLF_PARAM_ZVAL(2, zmin);
442+
Z_FLF_PARAM_ZVAL(3, zmax);
443+
444+
php_math_clamp(return_value, zvalue, zmin, zmax);
445+
}
446+
/* }}} */
447+
392448
/* {{{ Returns the sine of the number in radians */
393449
PHP_FUNCTION(sin)
394450
{

ext/standard/tests/math/clamp.phpt

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
--TEST--
2+
clamp() tests
3+
--INI--
4+
precision=14
5+
date.timezone=UTC
6+
zend.assertions=1
7+
--FILE--
8+
<?php
9+
10+
function make_clamp_fcc() {
11+
return clamp(...);
12+
}
13+
14+
function check_clamp_result($value, $min, $max) {
15+
$flf = clamp($value, $min, $max);
16+
$dyn = make_clamp_fcc()($value, $min, $max);
17+
assert($flf === $dyn || (is_nan($flf) && is_nan($dyn)));
18+
19+
return $flf;
20+
}
21+
22+
function check_clamp_exception($value, $min, $max) {
23+
try {
24+
var_dump(clamp($value, $min, $max));
25+
} catch (ValueError $error) {
26+
echo $error->getMessage(), "\n";
27+
}
28+
29+
try {
30+
var_dump(make_clamp_fcc()($value, $min, $max));
31+
} catch (ValueError $error) {
32+
echo $error->getMessage(), "\n";
33+
}
34+
}
35+
36+
var_dump(check_clamp_result(2, 1, 3));
37+
var_dump(check_clamp_result(0, 1, 3));
38+
var_dump(check_clamp_result(6, 1, 3));
39+
var_dump(check_clamp_result(2, 1.3, 3.4));
40+
var_dump(check_clamp_result(2.5, 1, 3));
41+
var_dump(check_clamp_result(2.5, 1.3, 3.4));
42+
var_dump(check_clamp_result(0, 1.3, 3.4));
43+
var_dump(check_clamp_result(M_PI, -INF, INF));
44+
var_dump(check_clamp_result(NAN, 4, 6));
45+
var_dump(check_clamp_result("a", "c", "g"));
46+
var_dump(check_clamp_result("d", "c", "g"));
47+
echo check_clamp_result('2025-08-01', '2025-08-15', '2025-09-15'), "\n";
48+
echo check_clamp_result('2025-08-20', '2025-08-15', '2025-09-15'), "\n";
49+
echo check_clamp_result(new \DateTimeImmutable('2025-08-01'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n";
50+
echo check_clamp_result(new \DateTimeImmutable('2025-08-20'), new \DateTimeImmutable('2025-08-15'), new \DateTimeImmutable('2025-09-15'))->format('Y-m-d'), "\n";
51+
var_dump(check_clamp_result(null, -1, 1));
52+
var_dump(check_clamp_result(null, 1, 3));
53+
var_dump(check_clamp_result(null, -3, -1));
54+
var_dump(check_clamp_result(-9999, null, 10));
55+
var_dump(check_clamp_result(12, null, 10));
56+
57+
$a = new \InvalidArgumentException('a');
58+
$b = new \RuntimeException('b');
59+
$c = new \LogicException('c');
60+
echo check_clamp_result($a, $b, $c)::class, "\n";
61+
echo check_clamp_result($b, $a, $c)::class, "\n";
62+
echo check_clamp_result($c, $a, $b)::class, "\n";
63+
64+
check_clamp_exception(4, NAN, 6);
65+
check_clamp_exception(7, 6, NAN);
66+
check_clamp_exception(1, 3, 2);
67+
check_clamp_exception(-9999, 5, null);
68+
check_clamp_exception(12, -5, null);
69+
70+
?>
71+
--EXPECT--
72+
int(2)
73+
int(1)
74+
int(3)
75+
int(2)
76+
float(2.5)
77+
float(2.5)
78+
float(1.3)
79+
float(3.141592653589793)
80+
float(NAN)
81+
string(1) "c"
82+
string(1) "d"
83+
2025-08-15
84+
2025-08-20
85+
2025-08-15
86+
2025-08-20
87+
int(-1)
88+
int(1)
89+
int(-3)
90+
int(-9999)
91+
int(10)
92+
InvalidArgumentException
93+
RuntimeException
94+
LogicException
95+
clamp(): Argument #2 ($min) must not be NAN
96+
clamp(): Argument #2 ($min) must not be NAN
97+
clamp(): Argument #3 ($max) must not be NAN
98+
clamp(): Argument #3 ($max) must not be NAN
99+
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
100+
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
101+
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
102+
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
103+
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)
104+
clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)

0 commit comments

Comments
 (0)