@schukai/monster
Version:
Monster is a simple library for creating fast, robust and lightweight websites.
192 lines (176 loc) • 7.86 kB
JavaScript
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* Node module: @schukai/monster
*
* This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
* The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
*
* For those who do not wish to adhere to the AGPLv3, a commercial license is available.
* Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
* For more information about purchasing a commercial license, please contact schukai GmbH.
*
* SPDX-License-Identifier: AGPL-3.0
*/
export { generateRangeComparisonExpression };
/**
* The `generateRangeComparisonExpression()` function is a function that generates a string representation
* of a comparison expression based on a range of values. It takes three arguments:
*
* - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
* - valueName (required): a string representing the name of the value that is being compared to the range of values.
* - options (optional): an object containing additional options to customize the comparison expression.
*
* The generateRangeComparisonExpression() function returns a string representation of the comparison expression.
*
* ## Options
* The option parameter is an object that can have the following properties:
*
* urlEncode (boolean, default: false): if set to true, URL encodes the comparison operators.
* andOp (string, default: '&&'): the logical AND operator to use in the expression.
* orOp (string, default: '||'): the logical OR operator to use in the expression.
* eqOp (string, default: '=='): the equality operator to use in the expression.
* geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
* leOp (string, default: '<='): the less than or equal to operator to use in the expression.
* gtOp (string, default: '>'): the greater than operator to use in the expression.
* ltOp (string, default: '<'): the less than operator to use in the expression.
*
* Examples
*
* ```javascript
* const expression = '0-10,20-30';
* const valueName = 'age';
* const options = { urlEncode: true, andOp: 'and', orOp: 'or', eqOp: '=', geOp: '>=', leOp: '<=' };
* const comparisonExpression = generateRangeComparisonExpression(expression, valueName, options);
*
* console.log(comparisonExpression); // age%3E%3D0%20and%20age%3C%3D10%20or%20age%3E%3D20%20and%20age%3C%3D30
* ```
*
* In this example, the generateRangeComparisonExpression() function generates a string representation of the comparison
* expression for the expression and valueName parameters with the specified options. The resulting comparison
* expression is 'age>=0 and age<=10 or age>=20 and age<=30', URL encoded according to the urlEncode option.
*
* @param {string} expression - The string expression to generate the comparison for.
* @param {string} valueName - The name of the value to compare against.
* @param {Object} [options] - The optional parameters.
* @param {boolean} [options.urlEncode=false] - Whether to encode comparison operators for use in a URL.
* @param {string} [options.andOp='&&'] - The logical AND operator to use.
* @param {string} [options.orOp='||'] - The logical OR operator to use.
* @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
* @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
* @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
* @param {string} [options.gtOp='>'] - The comparison operator for greater than to use.
* @param {string} [options.ltOp='<'] - The comparison operator for less than to use.
* @return {string} The generated comparison expression.
* @throws {Error} If the input is invalid.
* @summary Generates a comparison expression based on a range of values.
*/
/**
* Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
* SPDX-License-Identifier: AGPL-3.0
*/
function generateRangeComparisonExpression(
expression,
valueName,
options = {},
) {
const {
urlEncode = false,
andOp = "&&",
orOp = "||",
eqOp = "==",
geOp = ">=",
leOp = "<=",
gtOp = ">",
ltOp = "<",
} = options;
if (
typeof expression !== "string" ||
typeof valueName !== "string" ||
!expression.trim()
) {
throw new Error("Invalid input");
}
const encode = (s) => (urlEncode ? encodeURIComponent(s) : s);
const and = encode(andOp);
const or = encode(orOp);
const space = urlEncode ? "%20" : " ";
function parseRange(range) {
range = range.trim();
if (!range) throw new Error("Empty range");
// Sonderfall: nur Bindestriche wie "--"
if (/^-+$/.test(range)) {
throw new Error(`Invalid range '${range}'`);
}
// Bereich: z.B. -10--5, 4-6
const rangeMatch = range.match(
/^(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)/,
);
if (rangeMatch) {
const sNum = parseFloat(rangeMatch[1]);
const eNum = parseFloat(rangeMatch[2]);
if (isNaN(sNum) || isNaN(eNum))
throw new Error(`Invalid value '${range}'`);
if (sNum > eNum) throw new Error(`Invalid range '${range}'`);
return `${valueName}${encode(geOp)}${sNum}${space}${and}${space}${valueName}${encode(leOp)}${eNum}`;
}
// Exklusiver Bereich: 4~6 → x>4 && x<6
const exclMatch = range.match(/^(-?\d+(?:\.\d+)?)\s*~\s*(-?\d+(?:\.\d+)?)/);
if (exclMatch) {
const sNum = parseFloat(exclMatch[1]);
const eNum = parseFloat(exclMatch[2]);
if (isNaN(sNum) || isNaN(eNum))
throw new Error(`Invalid value '${range}'`);
return `${valueName}${encode(gtOp)}${sNum}${space}${and}${space}${valueName}${encode(ltOp)}${eNum}`;
}
// Offene Intervalle exklusiv: 4~ → x>4, ~6 → x<6
const openRightExclMatch = range.match(/^(-?\d+(?:\.\d+)?)~$/);
if (openRightExclMatch) {
const sNum = parseFloat(openRightExclMatch[1]);
if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(gtOp)}${sNum}`;
}
const openLeftExclMatch = range.match(/^~(-?\d+(?:\.\d+)?)$/);
if (openLeftExclMatch) {
const eNum = parseFloat(openLeftExclMatch[1]);
if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(ltOp)}${eNum}`;
}
// Offene Intervalle inklusiv: 4- → x>=4, -6 → x<=6
const openRightInclMatch = range.match(/^(-?\d+(?:\.\d+)?)\-$/);
if (openRightInclMatch) {
const sNum = parseFloat(openRightInclMatch[1]);
if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(geOp)}${sNum}`;
}
const openLeftInclMatch = range.match(/^\-(-?\d+(?:\.\d+)?)$/);
if (openLeftInclMatch) {
const eNum = parseFloat(openLeftInclMatch[1]);
if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(leOp)}${eNum}`;
}
// <n oder >n
if (range.startsWith("<")) {
const n = parseFloat(range.slice(1));
if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(ltOp)}${n}`;
}
if (range.startsWith(">")) {
const n = parseFloat(range.slice(1));
if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
return `${valueName}${encode(gtOp)}${n}`;
}
// Einzelwert NUR, wenn wirklich reine Zahl
if (/^-?\d+(\.\d+)?$/.test(range)) {
const value = parseFloat(range);
return `${valueName}${encode(eqOp)}${value}`;
}
throw new Error(`Invalid value '${range}'`);
}
const parts = expression
.split(",")
.map((r) => r.trim())
.filter(Boolean)
.map(parseRange);
const sep = space + or + space;
return parts.length > 1 ? parts.map((p) => `(${p})`).join(sep) : parts[0];
}