Skip to content

Commit 62fb107

Browse files
vankopevilebottnawi
authored andcommitted
feat: "smart" numbers range
1 parent 338db8b commit 62fb107

File tree

4 files changed

+237
-8
lines changed

4 files changed

+237
-8
lines changed

src/ValidationError.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
const Range = require('./util/Range');
2+
13
const SPECIFICITY = {
24
type: 1,
35
not: 1,
@@ -348,21 +350,28 @@ class ValidationError extends Error {
348350

349351
if (likeNumber(schema) || likeInteger(schema)) {
350352
const hints = [];
353+
const range = new Range();
351354

352355
if (typeof schema.minimum === 'number') {
353-
hints.push(`should be >= ${schema.minimum}`);
356+
range.left(schema.minimum);
354357
}
355358

356359
if (typeof schema.exclusiveMinimum === 'number') {
357-
hints.push(`should be > ${schema.exclusiveMinimum}`);
360+
range.left(schema.exclusiveMinimum, true);
358361
}
359362

360363
if (typeof schema.maximum === 'number') {
361-
hints.push(`should be <= ${schema.maximum}`);
364+
range.right(schema.maximum);
362365
}
363366

364367
if (typeof schema.exclusiveMaximum === 'number') {
365-
hints.push(`should be > ${schema.exclusiveMaximum}`);
368+
range.right(schema.exclusiveMaximum, true);
369+
}
370+
371+
const rangeFormat = range.format();
372+
373+
if (rangeFormat) {
374+
hints.push(rangeFormat);
366375
}
367376

368377
if (typeof schema.multipleOf === 'number') {

src/util/Range.js

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
const left = Symbol('left');
2+
const right = Symbol('right');
3+
4+
class Range {
5+
static getOperator(side, exclusive) {
6+
if (side === 'left') {
7+
return exclusive ? '>' : '>=';
8+
}
9+
10+
return exclusive ? '<' : '<=';
11+
}
12+
13+
static formatRight(value, logic, exclusive) {
14+
if (logic === false) {
15+
return Range.formatLeft(value, !logic, !exclusive);
16+
}
17+
18+
return `should be ${Range.getOperator('right', exclusive)} ${value}`;
19+
}
20+
21+
static formatLeft(value, logic, exclusive) {
22+
if (logic === false) {
23+
return Range.formatRight(value, !logic, !exclusive);
24+
}
25+
26+
return `should be ${Range.getOperator('left', exclusive)} ${value}`;
27+
}
28+
29+
static formatRange(start, end, startExclusive, endExclusive, logic) {
30+
let result = 'should be';
31+
32+
result += ` ${Range.getOperator(
33+
logic ? 'left' : 'right',
34+
logic ? startExclusive : !startExclusive
35+
)} ${start} `;
36+
result += logic ? 'and' : 'or';
37+
result += ` ${Range.getOperator(
38+
logic ? 'right' : 'left',
39+
logic ? endExclusive : !endExclusive
40+
)} ${end}`;
41+
42+
return result;
43+
}
44+
45+
static getRangeValue(values, logic) {
46+
let minMax = logic ? Infinity : -Infinity;
47+
let j = -1;
48+
const predicate = logic
49+
? ([value]) => value <= minMax
50+
: ([value]) => value >= minMax;
51+
52+
for (let i = 0; i < values.length; i++) {
53+
if (predicate(values[i])) {
54+
minMax = values[i][0];
55+
j = i;
56+
}
57+
}
58+
59+
if (j > -1) {
60+
return values[j];
61+
}
62+
63+
return [Infinity, true];
64+
}
65+
66+
constructor() {
67+
this[left] = [];
68+
this[right] = [];
69+
}
70+
71+
left(value, exclusive = false) {
72+
this[left].push([value, exclusive]);
73+
}
74+
75+
right(value, exclusive = false) {
76+
this[right].push([value, exclusive]);
77+
}
78+
79+
format(logic = true) {
80+
const [start, leftExclusive] = Range.getRangeValue(this[left], logic);
81+
const [end, rightExclusive] = Range.getRangeValue(this[right], !logic);
82+
83+
if (!Number.isFinite(start) && !Number.isFinite(end)) {
84+
return '';
85+
}
86+
87+
if (leftExclusive === rightExclusive) {
88+
// e.g. 5 <= x <= 5
89+
if (leftExclusive === false && start === end) {
90+
return `should be ${logic ? '' : '!'}= ${start}`;
91+
}
92+
93+
// e.g. 4 < x < 6
94+
if (leftExclusive === true && start + 1 === end - 1) {
95+
return `should be ${logic ? '' : '!'}= ${start + 1}`;
96+
}
97+
}
98+
99+
if (Number.isFinite(start) && !Number.isFinite(end)) {
100+
return Range.formatLeft(start, logic, leftExclusive);
101+
}
102+
103+
if (!Number.isFinite(start) && Number.isFinite(end)) {
104+
return Range.formatRight(end, logic, rightExclusive);
105+
}
106+
107+
return Range.formatRange(start, end, leftExclusive, rightExclusive, logic);
108+
}
109+
}
110+
111+
module.exports = Range;

test/__snapshots__/index.test.js.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -749,7 +749,7 @@ exports[`Validation should fail validation for integer type 1`] = `
749749
750750
exports[`Validation should fail validation for integer with exclusive maximum 1`] = `
751751
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
752-
- configuration.integerWithExclusiveMaximum should be a integer (should be > 0)."
752+
- configuration.integerWithExclusiveMaximum should be a integer (should be < 0)."
753753
`;
754754
755755
exports[`Validation should fail validation for integer with exclusive maximum 2`] = `
@@ -769,7 +769,7 @@ exports[`Validation should fail validation for integer with exclusive minimum 2`
769769
770770
exports[`Validation should fail validation for integer with minimum 1`] = `
771771
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
772-
- configuration.integerWithMinimum should be a integer (should be >= 5, should be <= 20)."
772+
- configuration.integerWithMinimum should be a integer (should be >= 5 and <= 20)."
773773
`;
774774
775775
exports[`Validation should fail validation for integer with minimum and maximum 1`] = `
@@ -1050,7 +1050,7 @@ exports[`Validation should fail validation for multipleOf 1`] = `
10501050
10511051
exports[`Validation should fail validation for multipleOf with minimum and maximum 1`] = `
10521052
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
1053-
- configuration.multipleOfProp should be a number (should be >= 5, should be <= 20, should be multiple of 5)."
1053+
- configuration.multipleOfProp should be a number (should be >= 5 and <= 20, should be multiple of 5)."
10541054
`;
10551055
10561056
exports[`Validation should fail validation for multipleOf with type number 1`] = `
@@ -1299,7 +1299,7 @@ exports[`Validation should fail validation for number type 1`] = `
12991299
13001300
exports[`Validation should fail validation for number with minimum and maximum 1`] = `
13011301
"Invalid configuration object. Object has been initialised using a configuration object that does not match the API schema.
1302-
- configuration.numberWithMinimum should be a number (should be >= 5, should be <= 20)."
1302+
- configuration.numberWithMinimum should be a number (should be >= 5 and <= 20)."
13031303
`;
13041304
13051305
exports[`Validation should fail validation for object #2 1`] = `

test/range.test.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import Range from '../src/util/Range';
2+
3+
it('5 <= x <= 5', () => {
4+
const range = new Range();
5+
range.left(5);
6+
range.right(5);
7+
8+
expect(range.format()).toEqual('should be = 5');
9+
});
10+
11+
it('not 5 <= x <= 5', () => {
12+
const range = new Range();
13+
range.left(5);
14+
range.right(5);
15+
16+
expect(range.format(false)).toEqual('should be != 5');
17+
});
18+
19+
it('-1 < x < 1', () => {
20+
const range = new Range();
21+
range.left(-1, true);
22+
range.right(1, true);
23+
24+
expect(range.format()).toEqual('should be = 0');
25+
});
26+
27+
it('not -1 < x < 1', () => {
28+
const range = new Range();
29+
range.left(-1, true);
30+
range.right(1, true);
31+
32+
expect(range.format(false)).toEqual('should be != 0');
33+
});
34+
35+
it('not 0 < x <= 10', () => {
36+
const range = new Range();
37+
range.left(0, true);
38+
range.right(10, false);
39+
40+
expect(range.format(false)).toEqual('should be <= 0 or > 10');
41+
});
42+
43+
it('x > 1000', () => {
44+
const range = new Range();
45+
range.left(10000, false);
46+
range.left(1000, true);
47+
48+
expect(range.format(true)).toEqual('should be > 1000');
49+
});
50+
51+
it('x < 0', () => {
52+
const range = new Range();
53+
range.right(-1000, true);
54+
range.right(-0, true);
55+
56+
expect(range.format()).toEqual('should be < 0');
57+
});
58+
59+
it('x >= -1000', () => {
60+
const range = new Range();
61+
range.right(-1000, true);
62+
range.right(0, false);
63+
64+
// expect x >= -1000 since it covers bigger range. [-1000, Infinity] is greater than [0, Infinity]
65+
expect(range.format(false)).toEqual('should be >= -1000');
66+
});
67+
68+
it('x <= 0', () => {
69+
const range = new Range();
70+
range.left(0, true);
71+
range.left(-100, false);
72+
73+
// expect x <= 0 since it covers bigger range. [-Infinity, 0] is greater than [-Infinity, -100]
74+
expect(range.format(false)).toEqual('should be <= 0');
75+
});
76+
77+
it('Empty string for infinity range', () => {
78+
const range = new Range();
79+
80+
expect(range.format(false)).toEqual('');
81+
});
82+
83+
it('0 < x < 122', () => {
84+
const range = new Range();
85+
range.left(0, true);
86+
range.right(12, false);
87+
range.right(122, true);
88+
89+
expect(range.format()).toEqual('should be > 0 and < 122');
90+
});
91+
92+
it('-1 <= x < 10', () => {
93+
const range = new Range();
94+
range.left(-1, false);
95+
range.left(10, true);
96+
range.right(10, true);
97+
98+
expect(range.format()).toEqual('should be >= -1 and < 10');
99+
});
100+
101+
it('not 10 < x < 10', () => {
102+
const range = new Range();
103+
range.left(-1, false);
104+
range.left(10, true);
105+
range.right(10, true);
106+
107+
// expect x <= 10 since it covers bigger range. [-Infinity, 10] is greater than [-Infinity, -1]
108+
expect(range.format(false)).toEqual('should be <= 10 or >= 10');
109+
});

0 commit comments

Comments
 (0)