Skip to content

Commit cb85da6

Browse files
committed
Refactor clamp to follow Math.clamp proposal and improve tests
1 parent 6327f87 commit cb85da6

File tree

3 files changed

+117
-35
lines changed

3 files changed

+117
-35
lines changed

packages/math/clamp/README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@
22

33
## clamp(value, min, max) ⇒ <code>Number</code>
44
Clamps number within the inclusive `min` and `max` bounds, making sure it does not go beyond them on either side.
5-
If `min` is greater than `max` the parameters are swapped to support inverted ranges.
5+
6+
The mplementation is based on proposal for `Nmuber.prototype.clamp` (originally `Math.clamp`):
7+
https://tc39.es/proposal-math-clamp
68

79
**Returns**: <code>Number</code> - The clamped number.
810
**Throws**:
911

10-
- <code>TypeError</code> If one or more of the arguments passed is not a number.
12+
- <code>TypeError</code> If any of the arguments is not a number.
13+
- <code>RangeError</code> If `min` is greater than `max`.
1114

1215

1316
| Param | Type | Description |
@@ -30,9 +33,6 @@ clamp(-15, 0, 100);
3033
clamp(120, 0, 100);
3134
// => 100
3235

33-
clamp(-5, NaN, 5); // If any of lower or upper bound are `NaN`, they will be converted to `0`.
34-
// => 0
35-
36-
clamp(120, 100, 0); // The order of lower and upper bounds is reversed (100 > 0)
37-
// => 100
36+
clamp(NaN, 0, 100);
37+
// => NaN
3838
```

packages/math/clamp/clamp.js

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@
33
/**
44
* Clamps number within the inclusive `min` and `max` bounds,
55
* making sure it does not go beyond them on either side.
6-
* If `min` is greater than `max` the parameters are swapped to support inverted ranges.
6+
*
7+
* The mplementation is based on proposal for `Nmuber.prototype.clamp` (originally `Math.clamp`):
8+
* https://tc39.es/proposal-math-clamp
79
*
810
* @param {Number} value The number to clamp.
911
* @param {Number} min The lower bound.
1012
* @param {Number} max The upper bound.
11-
* @throws {TypeError} If one or more of the arguments passed is not a number.
13+
* @throws {TypeError} If any of the arguments is not a number.
14+
* @throws {RnageError} If `min` is greater than `max`.
1215
* @returns {Number} The clamped number.
1316
* @example
1417
*
@@ -24,26 +27,59 @@
2427
* clamp(120, 0, 100);
2528
* // => 100
2629
*
27-
* clamp(-5, NaN, 5); // If any of lower or upper bound are `NaN`, they will be converted to `0`.
28-
* // => 0
29-
*
30-
* clamp(120, 100, 0); // The order of lower and upper bounds is reversed (100 > 0)
31-
* // => 100
30+
* clamp(NaN, 0, 100);
31+
* // => NaN
3232
*/
33-
const clamp = (value, lower, upper) => {
34-
if (typeof value !== 'number' || typeof lower !== 'number' || typeof upper !== 'number') {
35-
throw new TypeError('Expected all arguments to be numbers');
33+
const clamp = (value, min, max) => {
34+
if (typeof value !== 'number') {
35+
throw new TypeError('value must be a Number');
36+
}
37+
38+
if (typeof min !== 'number') {
39+
throw new TypeError('min must be a Number');
40+
}
41+
42+
if (typeof max !== 'number') {
43+
throw new TypeError('max must be a Number');
44+
}
45+
46+
if (Number.isNaN(min) || Number.isNaN(max) || Number.isNaN(value)) {
47+
return NaN;
48+
}
49+
50+
if (min > max) {
51+
throw new RangeError('min cannot be greater than max');
52+
}
53+
54+
if (Object.is(min, max)) {
55+
return min;
56+
}
57+
58+
if (Object.is(value, -0) && Object.is(min, +0)) {
59+
return +0;
60+
}
61+
62+
if (Object.is(value, +0) && Object.is(min, -0)) {
63+
return +0;
64+
}
65+
66+
if (value < min) {
67+
return min;
68+
}
69+
70+
if (Object.is(value, -0) && Object.is(max, +0)) {
71+
return -0;
3672
}
3773

38-
if (lower !== lower) { // lower bound is not `NaN`
39-
lower = 0;
74+
if (Object.is(value, +0) && Object.is(max, -0)) {
75+
return -0;
4076
}
4177

42-
if (upper !== upper) { // upper bound is not `NaN`
43-
upper = 0;
78+
if (value > max) {
79+
return max;
4480
}
4581

46-
return Math.min(Math.max(value, Math.min(lower, upper)), Math.max(lower, upper));
82+
return value;
4783
};
4884

4985
module.exports = clamp;

packages/math/clamp/test.js

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,73 @@
11
const clamp = require('./clamp');
22

33
describe('Math/clamp', () => {
4-
it('clamps number within the inclusive lower and upper bounds', () => {
5-
expect(clamp(5, 0, 10)).toBe(5);
4+
it('throws TypeError when value is not a number', () => {
5+
expect(() => clamp('5', 1, 10)).toThrow(TypeError);
6+
expect(() => clamp(null, 1, 10)).toThrow(TypeError);
7+
expect(() => clamp(undefined, 1, 10)).toThrow(TypeError);
8+
});
69

7-
expect(clamp(10, -5, 5)).toBe(5);
10+
it('throws TypeError when min is not a number', () => {
11+
expect(() => clamp(5, '1', 10)).toThrow(TypeError);
12+
});
813

9-
expect(clamp(-10, -5, 5)).toBe(-5);
14+
it('throws TypeError when max is not a number', () => {
15+
expect(() => clamp(5, 1, '10')).toThrow(TypeError);
16+
});
17+
18+
it('returns NaN when min is NaN', () => {
19+
expect(clamp(5, NaN, 10)).toBeNaN();
20+
});
1021

11-
expect(clamp(-15, 0, 100)).toBe(0);
22+
it('returns NaN when max is NaN', () => {
23+
expect(clamp(5, 1, NaN)).toBeNaN();
24+
});
1225

13-
expect(clamp(120, 0, 100)).toBe(100);
26+
it('returns NaN when value is NaN', () => {
27+
expect(clamp(NaN, 1, 10)).toBeNaN();
28+
});
1429

15-
expect(clamp(-5, NaN, NaN)).toBe(0);
30+
it('throws RangeError when min > max', () => {
31+
expect(() => clamp(5, 10, 1)).toThrow(RangeError);
32+
});
1633

17-
expect(clamp(-5, NaN, 5)).toBe(0);
34+
it('returns min when min === max', () => {
35+
expect(clamp(42, 7, 7)).toBe(7);
36+
expect(clamp(-0, +0, +0)).toBe(+0);
37+
expect(clamp(+0, -0, -0)).toBe(-0);
38+
});
1839

19-
expect(clamp(-5, 0, NaN)).toBe(0);
40+
it('value -0 and min +0 -> +0', () => {
41+
const result = clamp(-0, +0, 10);
42+
expect(Object.is(result, +0)).toBe(true);
43+
});
2044

21-
expect(clamp(120, 100, 0)).toBe(100);
45+
it('value +0 and min -0 -> +0', () => {
46+
const result = clamp(+0, -0, 10);
47+
expect(Object.is(result, +0)).toBe(true);
48+
});
49+
50+
it('value < min -> min', () => {
51+
expect(clamp(-3, 1, 10)).toBe(1);
52+
});
53+
54+
it('value -0 and max +0 -> -0', () => {
55+
const result = clamp(-0, -1, +0);
56+
expect(Object.is(result, -0)).toBe(true);
57+
});
58+
59+
it('value +0 and max -0 -> -0', () => {
60+
const result = clamp(+0, -1, -0);
61+
expect(Object.is(result, -0)).toBe(true);
62+
});
63+
64+
it('value > max -> max', () => {
65+
expect(clamp(99, 1, 10)).toBe(10);
66+
});
2267

23-
expect(() => {
24-
return clamp(10, '-5', '5');
25-
}).toThrow(new TypeError('Expected all arguments to be numbers'));
68+
it('returns value when within [min, max]', () => {
69+
expect(clamp(5, 1, 10)).toBe(5);
70+
expect(clamp(1, 1, 10)).toBe(1);
71+
expect(clamp(10, 1, 10)).toBe(10);
2672
});
2773
});

0 commit comments

Comments
 (0)