|
|
'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; };
|