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