Spaces:
Paused
Paused
| ; | |
| 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; | |
| }; | |