457 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			457 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 | |
| };
 |