fediversity.eu/node_modules/@sideway/formula/lib/index.js

457 lines
13 KiB
JavaScript
Raw Permalink Normal View History

2024-03-26 16:28:28 +01:00
'use strict';
const internals = {
operators: ['!', '^', '*', '/', '%', '+', '-', '<', '<=', '>', '>=', '==', '!=', '&&', '||', '??'],
operatorCharacters: ['!', '^', '*', '/', '%', '+', '-', '<', '=', '>', '&', '|', '?'],
operatorsOrder: [['^'], ['*', '/', '%'], ['+', '-'], ['<', '<=', '>', '>='], ['==', '!='], ['&&'], ['||', '??']],
operatorsPrefix: ['!', 'n'],
literals: {
'"': '"',
'`': '`',
'\'': '\'',
'[': ']'
},
numberRx: /^(?:[0-9]*(\.[0-9]*)?){1}$/,
tokenRx: /^[\w\$\#\.\@\:\{\}]+$/,
symbol: Symbol('formula'),
settings: Symbol('settings')
};
exports.Parser = class {
constructor(string, options = {}) {
if (!options[internals.settings] &&
options.constants) {
for (const constant in options.constants) {
const value = options.constants[constant];
if (value !== null &&
!['boolean', 'number', 'string'].includes(typeof value)) {
throw new Error(`Formula constant ${constant} contains invalid ${typeof value} value type`);
}
}
}
this.settings = options[internals.settings] ? options : Object.assign({ [internals.settings]: true, constants: {}, functions: {} }, options);
this.single = null;
this._parts = null;
this._parse(string);
}
_parse(string) {
let parts = [];
let current = '';
let parenthesis = 0;
let literal = false;
const flush = (inner) => {
if (parenthesis) {
throw new Error('Formula missing closing parenthesis');
}
const last = parts.length ? parts[parts.length - 1] : null;
if (!literal &&
!current &&
!inner) {
return;
}
if (last &&
last.type === 'reference' &&
inner === ')') { // Function
last.type = 'function';
last.value = this._subFormula(current, last.value);
current = '';
return;
}
if (inner === ')') { // Segment
const sub = new exports.Parser(current, this.settings);
parts.push({ type: 'segment', value: sub });
}
else if (literal) {
if (literal === ']') { // Reference
parts.push({ type: 'reference', value: current });
current = '';
return;
}
parts.push({ type: 'literal', value: current }); // Literal
}
else if (internals.operatorCharacters.includes(current)) { // Operator
if (last &&
last.type === 'operator' &&
internals.operators.includes(last.value + current)) { // 2 characters operator
last.value += current;
}
else {
parts.push({ type: 'operator', value: current });
}
}
else if (current.match(internals.numberRx)) { // Number
parts.push({ type: 'constant', value: parseFloat(current) });
}
else if (this.settings.constants[current] !== undefined) { // Constant
parts.push({ type: 'constant', value: this.settings.constants[current] });
}
else { // Reference
if (!current.match(internals.tokenRx)) {
throw new Error(`Formula contains invalid token: ${current}`);
}
parts.push({ type: 'reference', value: current });
}
current = '';
};
for (const c of string) {
if (literal) {
if (c === literal) {
flush();
literal = false;
}
else {
current += c;
}
}
else if (parenthesis) {
if (c === '(') {
current += c;
++parenthesis;
}
else if (c === ')') {
--parenthesis;
if (!parenthesis) {
flush(c);
}
else {
current += c;
}
}
else {
current += c;
}
}
else if (c in internals.literals) {
literal = internals.literals[c];
}
else if (c === '(') {
flush();
++parenthesis;
}
else if (internals.operatorCharacters.includes(c)) {
flush();
current = c;
flush();
}
else if (c !== ' ') {
current += c;
}
else {
flush();
}
}
flush();
// Replace prefix - to internal negative operator
parts = parts.map((part, i) => {
if (part.type !== 'operator' ||
part.value !== '-' ||
i && parts[i - 1].type !== 'operator') {
return part;
}
return { type: 'operator', value: 'n' };
});
// Validate tokens order
let operator = false;
for (const part of parts) {
if (part.type === 'operator') {
if (internals.operatorsPrefix.includes(part.value)) {
continue;
}
if (!operator) {
throw new Error('Formula contains an operator in invalid position');
}
if (!internals.operators.includes(part.value)) {
throw new Error(`Formula contains an unknown operator ${part.value}`);
}
}
else if (operator) {
throw new Error('Formula missing expected operator');
}
operator = !operator;
}
if (!operator) {
throw new Error('Formula contains invalid trailing operator');
}
// Identify single part
if (parts.length === 1 &&
['reference', 'literal', 'constant'].includes(parts[0].type)) {
this.single = { type: parts[0].type === 'reference' ? 'reference' : 'value', value: parts[0].value };
}
// Process parts
this._parts = parts.map((part) => {
// Operators
if (part.type === 'operator') {
return internals.operatorsPrefix.includes(part.value) ? part : part.value;
}
// Literals, constants, segments
if (part.type !== 'reference') {
return part.value;
}
// References
if (this.settings.tokenRx &&
!this.settings.tokenRx.test(part.value)) {
throw new Error(`Formula contains invalid reference ${part.value}`);
}
if (this.settings.reference) {
return this.settings.reference(part.value);
}
return internals.reference(part.value);
});
}
_subFormula(string, name) {
const method = this.settings.functions[name];
if (typeof method !== 'function') {
throw new Error(`Formula contains unknown function ${name}`);
}
let args = [];
if (string) {
let current = '';
let parenthesis = 0;
let literal = false;
const flush = () => {
if (!current) {
throw new Error(`Formula contains function ${name} with invalid arguments ${string}`);
}
args.push(current);
current = '';
};
for (let i = 0; i < string.length; ++i) {
const c = string[i];
if (literal) {
current += c;
if (c === literal) {
literal = false;
}
}
else if (c in internals.literals &&
!parenthesis) {
current += c;
literal = internals.literals[c];
}
else if (c === ',' &&
!parenthesis) {
flush();
}
else {
current += c;
if (c === '(') {
++parenthesis;
}
else if (c === ')') {
--parenthesis;
}
}
}
flush();
}
args = args.map((arg) => new exports.Parser(arg, this.settings));
return function (context) {
const innerValues = [];
for (const arg of args) {
innerValues.push(arg.evaluate(context));
}
return method.call(context, ...innerValues);
};
}
evaluate(context) {
const parts = this._parts.slice();
// Prefix operators
for (let i = parts.length - 2; i >= 0; --i) {
const part = parts[i];
if (part &&
part.type === 'operator') {
const current = parts[i + 1];
parts.splice(i + 1, 1);
const value = internals.evaluate(current, context);
parts[i] = internals.single(part.value, value);
}
}
// Left-right operators
internals.operatorsOrder.forEach((set) => {
for (let i = 1; i < parts.length - 1;) {
if (set.includes(parts[i])) {
const operator = parts[i];
const left = internals.evaluate(parts[i - 1], context);
const right = internals.evaluate(parts[i + 1], context);
parts.splice(i, 2);
const result = internals.calculate(operator, left, right);
parts[i - 1] = result === 0 ? 0 : result; // Convert -0
}
else {
i += 2;
}
}
});
return internals.evaluate(parts[0], context);
}
};
exports.Parser.prototype[internals.symbol] = true;
internals.reference = function (name) {
return function (context) {
return context && context[name] !== undefined ? context[name] : null;
};
};
internals.evaluate = function (part, context) {
if (part === null) {
return null;
}
if (typeof part === 'function') {
return part(context);
}
if (part[internals.symbol]) {
return part.evaluate(context);
}
return part;
};
internals.single = function (operator, value) {
if (operator === '!') {
return value ? false : true;
}
// operator === 'n'
const negative = -value;
if (negative === 0) { // Override -0
return 0;
}
return negative;
};
internals.calculate = function (operator, left, right) {
if (operator === '??') {
return internals.exists(left) ? left : right;
}
if (typeof left === 'string' ||
typeof right === 'string') {
if (operator === '+') {
left = internals.exists(left) ? left : '';
right = internals.exists(right) ? right : '';
return left + right;
}
}
else {
switch (operator) {
case '^': return Math.pow(left, right);
case '*': return left * right;
case '/': return left / right;
case '%': return left % right;
case '+': return left + right;
case '-': return left - right;
}
}
switch (operator) {
case '<': return left < right;
case '<=': return left <= right;
case '>': return left > right;
case '>=': return left >= right;
case '==': return left === right;
case '!=': return left !== right;
case '&&': return left && right;
case '||': return left || right;
}
return null;
};
internals.exists = function (value) {
return value !== null && value !== undefined;
};