Skip to content

Commit

Permalink
Allow undefined and null to mean "no bound" and reordered arguments t…
Browse files Browse the repository at this point in the history
…o (number, min, max)

Signed-off-by: Richie Bendall <[email protected]>
  • Loading branch information
Richienb committed Apr 6, 2024
1 parent 6d1aee5 commit eaa8154
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 69 deletions.
18 changes: 13 additions & 5 deletions implementation.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ import esAbstract from 'es-abstract';

const {ToNumber: toNumber} = esAbstract;

export default function clamp(min, val, max) {
const minCoerced = toNumber(min);
const valCoerced = toNumber(val);
const maxCoerced = toNumber(max);
export default function clamp(number, min, max) {
number = toNumber(number);

return Math.max(minCoerced, Math.min(valCoerced, maxCoerced));
if (max !== undefined && max !== null) {
max = toNumber(max);
number = Math.min(number, max);
}

if (min !== undefined && min !== null) {
min = toNumber(min);
number = Math.max(number, min);
}

return number;
}
13 changes: 7 additions & 6 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -2407,22 +2407,22 @@
<li><span>Toggle "can call user code" annotations</span><code>u</code></li>

<li><span>Jump to search box</span><code>/</code></li>
</ul></div><div id="spec-container"><h1 class="version">Stage 0 Draft / April 22, 2023</h1><h1 class="title">Math.clamp</h1>
</ul></div><div id="spec-container"><h1 class="version">Stage 0 Draft / April 6, 2024</h1><h1 class="title">Math.clamp</h1>

<emu-intro id="intro">
<h1>Introduction</h1>
<p><code>Math.clamp()</code> constrains a value between an upper and lower bound.</p>
</emu-intro>

<emu-clause id="sec-math.clamp">
<h1><span class="secnum">1</span> Math.clamp ( <var>min</var>, <var>val</var>, <var>max</var> )</h1>
<p>The <code>clamp</code> function returns a number that is the result of constraining <var>number</var> between the lower bound defined by <var>min</var> and the upper bound defined by <var>max</var>.</p>
<emu-alg><ol><li>Let <var>minCoerced</var> be ?&nbsp;<emu-xref aoid="ToNumber" id="_ref_0"><a href="https://tc39.es/ecma262/#sec-tonumber">ToNumber</a></emu-xref>(<var>min</var>).</li><li>Let <var>valCoerced</var> be ?&nbsp;<emu-xref aoid="ToNumber" id="_ref_1"><a href="https://tc39.es/ecma262/#sec-tonumber">ToNumber</a></emu-xref>(<var>val</var>).</li><li>Let <var>maxCoerced</var> be ?&nbsp;<emu-xref aoid="ToNumber" id="_ref_2"><a href="https://tc39.es/ecma262/#sec-tonumber">ToNumber</a></emu-xref>(<var>max</var>).</li><li>Return <emu-xref aoid="max"><a href="https://tc39.es/ecma262/#eqn-max">max</a></emu-xref>(<var>minCoerced</var>, <emu-xref aoid="min"><a href="https://tc39.es/ecma262/#eqn-min">min</a></emu-xref>(<var>valCoerced</var>, <var>maxCoerced</var>)).</li></ol></emu-alg>
<h1><span class="secnum">1</span> Math.clamp ( <var>number</var>, <var>min</var>, <var>max</var> )</h1>
<p>This function returns the <emu-xref href="#number-value"><a href="https://tc39.es/ecma262/#number-value">Number value</a></emu-xref> that is the result of constraining <var>number</var> between the bounds defined by <var>min</var> and <var>max</var>.</p>
<emu-alg><ol><li>Set <var>number</var> to ?&nbsp;<emu-xref aoid="ToNumber" id="_ref_0"><a href="https://tc39.es/ecma262/#sec-tonumber">ToNumber</a></emu-xref>(<var>number</var>).</li><li>If <var>max</var> is neither <emu-val>undefined</emu-val> nor <emu-val>null</emu-val>, then<ol><li>Set <var>max</var> to ?&nbsp;<emu-xref aoid="ToNumber" id="_ref_1"><a href="https://tc39.es/ecma262/#sec-tonumber">ToNumber</a></emu-xref>(<var>max</var>).</li><li>Set <var>number</var> to <emu-xref aoid="min"><a href="https://tc39.es/ecma262/#eqn-min">min</a></emu-xref>(<var>number</var>, <var>max</var>).</li></ol></li><li>If <var>min</var> is neither <emu-val>undefined</emu-val> nor <emu-val>null</emu-val>, then<ol><li>Set <var>min</var> to ?&nbsp;<emu-xref aoid="ToNumber" id="_ref_2"><a href="https://tc39.es/ecma262/#sec-tonumber">ToNumber</a></emu-xref>(<var>min</var>).</li><li>Set <var>number</var> to <emu-xref aoid="max"><a href="https://tc39.es/ecma262/#eqn-max">max</a></emu-xref>(<var>number</var>, <var>min</var>).</li></ol></li><li>Return <var>number</var>.</li></ol></emu-alg>
</emu-clause><emu-annex id="sec-copyright-and-software-license">
<h1><span class="secnum">A</span> Copyright &amp; Software License</h1>

<h2>Copyright Notice</h2>
<p2023 Richie Bendall</p>
<p2024 Richie Bendall</p>

<h2>Software License</h2>
<p>All Software contained in this document ("Software") is protected by copyright and is being made available under the "BSD License", included below. This Software may be subject to third party rights (rights from parties other than Ecma International), including patent rights, and no licenses under such third party rights are granted under this license even if the third party concerned is a member of Ecma International. SEE THE ECMA CODE OF CONDUCT IN PATENT MATTERS AVAILABLE AT https://ecma-international.org/memento/codeofconduct.htm FOR INFORMATION REGARDING THE LICENSING OF PATENT CLAIMS THAT ARE REQUIRED TO IMPLEMENT ECMA INTERNATIONAL STANDARDS.</p>
Expand All @@ -2437,4 +2437,5 @@ <h2>Software License</h2>

<p>THIS SOFTWARE IS PROVIDED BY THE ECMA INTERNATIONAL "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL ECMA INTERNATIONAL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.</p>

</emu-annex></div></body>
</emu-annex>
</div></body>
91 changes: 44 additions & 47 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,7 @@ ECMAScript proposal and reference implementation for `Math.clamp`.

A [clamping function](https://en.wikipedia.org/wiki/Clamping_(graphics)) constrains a value between an upper and lower bound.

The primary motivation for this function is to bring parity with the [CSS function][css-clamp] of the same name. It will be useful when using it in conjunction with the [CSS Painting API](https://developer.mozilla.org/en-US/docs/Web/API/CSS_Painting_API).

## Examples

Currently the problem is solved in two ways:
Our primary motivation is its usefulness and [popularity](https://github.com/search?q=clamp+language%3AJavaScript+&type=code) in existing projects where it is often defined for the sake of readability. A common use is for animations and interactive content. For example, it helps to keep objects in-bounds during user-controlled movement by restricting the coordinates that it can move to (see the [p5.js demo](https://p5js.org/reference/#/p5/constrain) for its `constrain` function). Projects tend to define a function that looks like `clamp(number, min, max)`, either through:

- Chaining mathematical operators with if statements or ternary operators

Expand All @@ -42,66 +38,66 @@ function clamp(number, minimum, maximum) {
}
```

Each of these require a unnecessary boilerplate and are error-prone.
Each of those examples require unnecessary boilerplate and are error-prone. For example, a developer only has to mistype a single operator or mix up a single variable name for the function to break. They also disregard the potential undefined behaviour that can occur when minimum is larger than maximum, or when only `min` or `max` is specified.

For example, a developer only has to mistype a single operator or mix up a single variable name for the function to break.
We name it the function `clamp`, like how it is in other programming languages...

Both of these common strategies also disregard the potential undefined behaviour that can occur when `minimum` is larger than `maximum`.
- **[`clamp`][css-clamp] in CSS**
- [`Math.Clamp`](https://docs.microsoft.com/en-us/dotnet/api/system.math.clamp?view=netcore-2.0) in C#
- [`std::clamp`](https://en.cppreference.com/w/cpp/algorithm/clamp) in the C++ standard library
- `clamp` for [`f32`](https://doc.rust-lang.org/std/primitive.f32.html#method.clamp) and [`f64`](https://doc.rust-lang.org/std/primitive.f64.html#method.clamp) in Rust
- [`coerceIn`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/coerce-in.html) in Kotlin
- [`clamp`](https://api.dart.dev/stable/2.14.4/dart-core/num/clamp.html) in Dart
- [`clamp`](https://ruby-doc.org/core-2.4.0/Comparable.html#method-i-clamp) in Ruby
- [`clamp`](https://package.elm-lang.org/packages/elm/core/latest/Basics#clamp) in Elm

The proposed API allows a developer to clamp numbers without creating a separate function:
...and userland implementations:

- [`clamp`](https://github.com/hughsk/clamp/blob/377851f0cca9f3f134b53881e294782cccdae4d8/index.js#L3-L7)
- [`math-clamp`][math-clamp]
- [`lodash`](https://github.com/lodash/lodash/blob/bb7c95947914d12af5f79e7369dd59ce29bc61a8/clamp.js)
- [`three.js`](https://github.com/mrdoob/three.js/blob/431baa0a0e808637df959aa547c98e0b2380bdbe/src/math/MathUtils.js#L43-L47)
- [`p5.js`](https://github.com/processing/p5.js/blob/098f36ded792fca894fdfd947d3293db5bb35e79/src/math/calculation.js#L111-L114)
- [`ramda`](https://github.com/ramda/ramda/blob/6b6a85d3fe30ac1a41ac05734be9f61bd92325e5/source/clamp.js#L23-L32)
- [`sugar`](https://github.com/andrewplummer/Sugar/blob/3ca57818332473b601434001ac1445552d7753ff/lib/range.js#L164-L178)
- [`phaser`](https://github.com/photonstorm/phaser/blob/29ada646e00ebdd375a31eee871be5b10286ba46/src/math/Clamp.js#L19-L22)

Another motivation is to bring parity with the [CSS function][css-clamp] of the same name, although it will have a different parameter order because of the slightly different use cases in each context (also see [the previous discussion on the order of options for CSS `clamp`](https://github.com/w3c/csswg-drafts/issues/2519#issuecomment-387803089).

Recognizing the usefulness and improved readability of only specifying either a `min` or `max` (i.e. `Math.min(number, 5)` vs `Math.clamp(number, undefined, 5)`), we consider also allowing `null` or `undefined` as values for `min` and `max` to semantically mean "no upper/lower bound". We consider this as opposed to `Math.clampMin` and `Math.clampMax`, or an optional bag, to remain conventional to the language.

## Examples

The proposed API allows a developer to clamp numbers like:

```js
Math.clamp(0, 5, 10);
Math.clamp(5, 0, 10);
//=> 5

Math.clamp(0, -5, 10);
Math.clamp(-5, 0, 10);
//=> 0

Math.clamp(0, 15, 10);
Math.clamp(15, 0, 10);
//=> 10
```

It also handles a larger minimum than maximum number, analogous to the [CSS `clamp()` function][css-clamp-spec].
It handles a larger minimum than maximum number, like how the [CSS `clamp()` function][css-clamp-spec] does.

```js
// Minimum number is larger than maximum value
Math.clamp(10, 5, 0);
//=> 10
```

For reasoning on the order of arguments, see [the previous discussion](https://github.com/w3c/csswg-drafts/issues/2519#issuecomment-387803089) for the CSS function of the same name.

### Real-world scenarios

A common use is for animations and interactive content.
It supports `null`/`undefined`/`-Infinity`/`-Infinity` to specify when there is no upper or lower bound:

For example, it helps to keep objects in-bounds during user-controlled movement by restricting the coordinates that it can move to.

See the [p5.js demo](https://p5js.org/reference/#/p5/constrain) for its `constrain` function.

## Userland implementations

- [`clamp`](https://github.com/hughsk/clamp/blob/377851f0cca9f3f134b53881e294782cccdae4d8/index.js#L3-L7)
- [`math-clamp`][math-clamp]
- [`lodash`](https://github.com/lodash/lodash/blob/bb7c95947914d12af5f79e7369dd59ce29bc61a8/clamp.js)
- [`three.js`](https://github.com/mrdoob/three.js/blob/431baa0a0e808637df959aa547c98e0b2380bdbe/src/math/MathUtils.js#L43-L47)
- [`p5.js`](https://github.com/processing/p5.js/blob/098f36ded792fca894fdfd947d3293db5bb35e79/src/math/calculation.js#L111-L114)
- [`ramda`](https://github.com/ramda/ramda/blob/6b6a85d3fe30ac1a41ac05734be9f61bd92325e5/source/clamp.js#L23-L32)
- [`sugar`](https://github.com/andrewplummer/Sugar/blob/3ca57818332473b601434001ac1445552d7753ff/lib/range.js#L164-L178)
- [`phaser`](https://github.com/photonstorm/phaser/blob/29ada646e00ebdd375a31eee871be5b10286ba46/src/math/Clamp.js#L19-L22)

## Naming in other languages

Similar functionality exists in other languages with most using a similar name. This is why the function will also be called `clamp`.
```js
Math.clamp(5, 0, null);
//=> 5

- **[`clamp`][css-clamp] in CSS**
- [`Math.Clamp`](https://docs.microsoft.com/en-us/dotnet/api/system.math.clamp?view=netcore-2.0) in C#
- [`std::clamp`](https://en.cppreference.com/w/cpp/algorithm/clamp) in the C++ standard library
- `clamp` for [`f32`](https://doc.rust-lang.org/std/primitive.f32.html#method.clamp) and [`f64`](https://doc.rust-lang.org/std/primitive.f64.html#method.clamp) in Rust
- [`coerceIn`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.ranges/coerce-in.html) in Kotlin
- [`clamp`](https://api.dart.dev/stable/2.14.4/dart-core/num/clamp.html) in Dart
- [`clamp`](https://ruby-doc.org/core-2.4.0/Comparable.html#method-i-clamp) in Ruby
- [`clamp`](https://package.elm-lang.org/packages/elm/core/latest/Basics#clamp) in Elm
Math.clamp(5, undefined, 10);
//=> 5
```

## Specification

Expand All @@ -112,10 +108,11 @@ Similar functionality exists in other languages with most using a similar name.

- [Reference Polyfill](polyfill.js)

## Credits
## Acknowledgements

- Specification improved from the [`Math` Extensions Proposal](https://github.com/rwaldron/proposal-math-extensions)
- Specification and reference implementation further inspired by [`math-clamp`][math-clamp]
Specification and implementation inspired by:
- [`Math` Extensions Proposal](https://github.com/rwaldron/proposal-math-extensions)
- [`math-clamp`][math-clamp]

[math-clamp]: https://github.com/sindresorhus/math-clamp/blob/3897064dd3e9711a2e47e891d0aa7eb66ccdcef8/index.js#L1-L15
[math-min]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min
Expand Down
18 changes: 11 additions & 7 deletions spec.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,16 @@ <h1>Introduction</h1>
</emu-intro>

<emu-clause id="sec-math.clamp">
<h1>Math.clamp ( _min_, _val_, _max_ )</h1>
<p>The `clamp` function returns a number that is the result of constraining _number_ between the lower bound defined by _min_ and the upper bound defined by _max_.</p>
<h1>Math.clamp ( _number_, _min_, _max_ )</h1>
<p>This function returns the Number value that is the result of constraining _number_ between the bounds defined by _min_ and _max_.</p>
<emu-alg>
1. Let _minCoerced_ be ? ToNumber(_min_).
1. Let _valCoerced_ be ? ToNumber(_val_).
1. Let _maxCoerced_ be ? ToNumber(_max_).
1. Return max(_minCoerced_, min(_valCoerced_, _maxCoerced_)).
1. Set _number_ to ? ToNumber(_number_).
1. If _max_ is neither *undefined* nor *null*, then
1. Set _max_ to ? ToNumber(_max_).
1. Set _number_ to min(_number_, _max_).
1. If _min_ is neither *undefined* nor *null*, then
1. Set _min_ to ? ToNumber(_min_).
1. Set _number_ to max(_number_, _min_).
1. Return _number_.
</emu-alg>
</emu-clause>
</emu-clause>
14 changes: 10 additions & 4 deletions test.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ console.assert(!configurable, 'Math.clamp is configurable');
console.assert(!enumerable, 'Math.clamp is enumerable');
console.assert(!writable, 'Math.clamp is writable');

console.assert(Math.clamp(0, 5, 10) === 5, 'Math.clamp(0, 5, 10) === 5');
console.assert(Math.clamp(0, -5, 10) === 0, 'Math.clamp(0, -5, 10) === 0');
console.assert(Math.clamp(0, 15, 10) === 10, 'Math.clamp(0, 15, 10) === 10');
console.assert(Math.clamp(10, 5, 0) === 10, 'Math.clamp(10, 5, 0) === 10');
console.assert(Math.clamp(5, 0, 10) === 5, 'Math.clamp(5, 0, 10) === 5');
console.assert(Math.clamp(-5, 0, 10) === 0, 'Math.clamp(-5, 0, 10) === 0');
console.assert(Math.clamp(15, 0, 10) === 10, 'Math.clamp(15, 0, 10) === 10');
console.assert(Math.clamp(5, 10, 0) === 10, 'Math.clamp(5, 10, 0) === 10');
console.assert(Math.clamp(5, undefined, 10) === 5, 'Math.clamp(5, undefined, 10) === 5');
console.assert(Math.clamp(5, 0, undefined) === 5, 'Math.clamp(5, 0, undefined) === 5');
console.assert(Math.clamp(5, undefined, undefined) === 5, 'Math.clamp(5, undefined, undefined) === 5');
console.assert(Math.clamp(5, null, 10) === 5, 'Math.clamp(5, null, 10) === 5');
console.assert(Math.clamp(5, 0, null) === 5, 'Math.clamp(5, 0, null) === 5');
console.assert(Math.clamp(5, null, null) === 5, 'Math.clamp(5, null, null) === 5');

0 comments on commit eaa8154

Please sign in to comment.