From eaa81541768eccba20edf16a4635ee703de970ab Mon Sep 17 00:00:00 2001 From: Richie Bendall Date: Sat, 6 Apr 2024 16:55:55 +1300 Subject: [PATCH] Allow undefined and null to mean "no bound" and reordered arguments to (number, min, max) Signed-off-by: Richie Bendall --- implementation.js | 18 +++++++--- index.html | 13 +++---- readme.md | 91 +++++++++++++++++++++++------------------------ spec.html | 18 ++++++---- test.js | 14 +++++--- 5 files changed, 85 insertions(+), 69 deletions(-) diff --git a/implementation.js b/implementation.js index 110ccd3..f83bc77 100644 --- a/implementation.js +++ b/implementation.js @@ -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; } diff --git a/index.html b/index.html index 674834d..39b5ee7 100644 --- a/index.html +++ b/index.html @@ -2407,7 +2407,7 @@
  • Toggle "can call user code" annotationsu
  • Jump to search box/
  • -

    Stage 0 Draft / April 22, 2023

    Math.clamp

    +

    Stage 0 Draft / April 6, 2024

    Math.clamp

    Introduction

    @@ -2415,14 +2415,14 @@

    Introduction

    -

    1 Math.clamp ( min, val, max )

    -

    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.

    -
    1. Let minCoerced be ? ToNumber(min).
    2. Let valCoerced be ? ToNumber(val).
    3. Let maxCoerced be ? ToNumber(max).
    4. Return max(minCoerced, min(valCoerced, maxCoerced)).
    +

    1 Math.clamp ( number, min, max )

    +

    This function returns the Number value that is the result of constraining number between the bounds defined by min and max.

    +
    1. Set number to ? ToNumber(number).
    2. If max is neither undefined nor null, then
      1. Set max to ? ToNumber(max).
      2. Set number to min(number, max).
    3. If min is neither undefined nor null, then
      1. Set min to ? ToNumber(min).
      2. Set number to max(number, min).
    4. Return number.

    A Copyright & Software License

    Copyright Notice

    -

    © 2023 Richie Bendall

    +

    © 2024 Richie Bendall

    Software License

    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.

    @@ -2437,4 +2437,5 @@

    Software License

    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.

    -
    \ No newline at end of file + + \ No newline at end of file diff --git a/readme.md b/readme.md index b305729..9dc1ee8 100644 --- a/readme.md +++ b/readme.md @@ -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 @@ -42,26 +38,50 @@ 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 @@ -69,39 +89,15 @@ 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 @@ -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 diff --git a/spec.html b/spec.html index 0c1321e..d8b4cf3 100644 --- a/spec.html +++ b/spec.html @@ -11,12 +11,16 @@

    Introduction

    -

    Math.clamp ( _min_, _val_, _max_ )

    -

    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_.

    +

    Math.clamp ( _number_, _min_, _max_ )

    +

    This function returns the Number value that is the result of constraining _number_ between the bounds defined by _min_ and _max_.

    - 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_. -
    \ No newline at end of file + diff --git a/test.js b/test.js index 908bc62..10f63ea 100644 --- a/test.js +++ b/test.js @@ -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');