You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
163 lines
4.0 KiB
163 lines
4.0 KiB
1 month ago
|
"use strict";
|
||
|
|
||
|
/**
|
||
|
* @typedef {[number, boolean]} RangeValue
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* @callback RangeValueCallback
|
||
|
* @param {RangeValue} rangeValue
|
||
|
* @returns {boolean}
|
||
|
*/
|
||
|
class Range {
|
||
|
/**
|
||
|
* @param {"left" | "right"} side
|
||
|
* @param {boolean} exclusive
|
||
|
* @returns {">" | ">=" | "<" | "<="}
|
||
|
*/
|
||
|
static getOperator(side, exclusive) {
|
||
|
if (side === 'left') {
|
||
|
return exclusive ? '>' : '>=';
|
||
|
}
|
||
|
|
||
|
return exclusive ? '<' : '<=';
|
||
|
}
|
||
|
/**
|
||
|
* @param {number} value
|
||
|
* @param {boolean} logic is not logic applied
|
||
|
* @param {boolean} exclusive is range exclusive
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
|
||
|
|
||
|
static formatRight(value, logic, exclusive) {
|
||
|
if (logic === false) {
|
||
|
return Range.formatLeft(value, !logic, !exclusive);
|
||
|
}
|
||
|
|
||
|
return `should be ${Range.getOperator('right', exclusive)} ${value}`;
|
||
|
}
|
||
|
/**
|
||
|
* @param {number} value
|
||
|
* @param {boolean} logic is not logic applied
|
||
|
* @param {boolean} exclusive is range exclusive
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
|
||
|
|
||
|
static formatLeft(value, logic, exclusive) {
|
||
|
if (logic === false) {
|
||
|
return Range.formatRight(value, !logic, !exclusive);
|
||
|
}
|
||
|
|
||
|
return `should be ${Range.getOperator('left', exclusive)} ${value}`;
|
||
|
}
|
||
|
/**
|
||
|
* @param {number} start left side value
|
||
|
* @param {number} end right side value
|
||
|
* @param {boolean} startExclusive is range exclusive from left side
|
||
|
* @param {boolean} endExclusive is range exclusive from right side
|
||
|
* @param {boolean} logic is not logic applied
|
||
|
* @returns {string}
|
||
|
*/
|
||
|
|
||
|
|
||
|
static formatRange(start, end, startExclusive, endExclusive, logic) {
|
||
|
let result = 'should be';
|
||
|
result += ` ${Range.getOperator(logic ? 'left' : 'right', logic ? startExclusive : !startExclusive)} ${start} `;
|
||
|
result += logic ? 'and' : 'or';
|
||
|
result += ` ${Range.getOperator(logic ? 'right' : 'left', logic ? endExclusive : !endExclusive)} ${end}`;
|
||
|
return result;
|
||
|
}
|
||
|
/**
|
||
|
* @param {Array<RangeValue>} values
|
||
|
* @param {boolean} logic is not logic applied
|
||
|
* @return {RangeValue} computed value and it's exclusive flag
|
||
|
*/
|
||
|
|
||
|
|
||
|
static getRangeValue(values, logic) {
|
||
|
let minMax = logic ? Infinity : -Infinity;
|
||
|
let j = -1;
|
||
|
const predicate = logic ?
|
||
|
/** @type {RangeValueCallback} */
|
||
|
([value]) => value <= minMax :
|
||
|
/** @type {RangeValueCallback} */
|
||
|
([value]) => value >= minMax;
|
||
|
|
||
|
for (let i = 0; i < values.length; i++) {
|
||
|
if (predicate(values[i])) {
|
||
|
[minMax] = values[i];
|
||
|
j = i;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (j > -1) {
|
||
|
return values[j];
|
||
|
}
|
||
|
|
||
|
return [Infinity, true];
|
||
|
}
|
||
|
|
||
|
constructor() {
|
||
|
/** @type {Array<RangeValue>} */
|
||
|
this._left = [];
|
||
|
/** @type {Array<RangeValue>} */
|
||
|
|
||
|
this._right = [];
|
||
|
}
|
||
|
/**
|
||
|
* @param {number} value
|
||
|
* @param {boolean=} exclusive
|
||
|
*/
|
||
|
|
||
|
|
||
|
left(value, exclusive = false) {
|
||
|
this._left.push([value, exclusive]);
|
||
|
}
|
||
|
/**
|
||
|
* @param {number} value
|
||
|
* @param {boolean=} exclusive
|
||
|
*/
|
||
|
|
||
|
|
||
|
right(value, exclusive = false) {
|
||
|
this._right.push([value, exclusive]);
|
||
|
}
|
||
|
/**
|
||
|
* @param {boolean} logic is not logic applied
|
||
|
* @return {string} "smart" range string representation
|
||
|
*/
|
||
|
|
||
|
|
||
|
format(logic = true) {
|
||
|
const [start, leftExclusive] = Range.getRangeValue(this._left, logic);
|
||
|
const [end, rightExclusive] = Range.getRangeValue(this._right, !logic);
|
||
|
|
||
|
if (!Number.isFinite(start) && !Number.isFinite(end)) {
|
||
|
return '';
|
||
|
}
|
||
|
|
||
|
const realStart = leftExclusive ? start + 1 : start;
|
||
|
const realEnd = rightExclusive ? end - 1 : end; // e.g. 5 < x < 7, 5 < x <= 6, 6 <= x <= 6
|
||
|
|
||
|
if (realStart === realEnd) {
|
||
|
return `should be ${logic ? '' : '!'}= ${realStart}`;
|
||
|
} // e.g. 4 < x < ∞
|
||
|
|
||
|
|
||
|
if (Number.isFinite(start) && !Number.isFinite(end)) {
|
||
|
return Range.formatLeft(start, logic, leftExclusive);
|
||
|
} // e.g. ∞ < x < 4
|
||
|
|
||
|
|
||
|
if (!Number.isFinite(start) && Number.isFinite(end)) {
|
||
|
return Range.formatRight(end, logic, rightExclusive);
|
||
|
}
|
||
|
|
||
|
return Range.formatRange(start, end, leftExclusive, rightExclusive, logic);
|
||
|
}
|
||
|
|
||
|
}
|
||
|
|
||
|
module.exports = Range;
|