superfly-timeline
Version:
Resolver for defining objects with temporal boolean logic relationships on a timeline
183 lines • 7.87 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ExpressionHandler = exports.REGEXP_OPERATORS = exports.OPERATORS = void 0;
const lib_1 = require("./lib/lib");
const cache_1 = require("./lib/cache");
const expression_1 = require("./lib/expression");
exports.OPERATORS = ['&', '|', '+', '-', '*', '/', '%', '!'];
exports.REGEXP_OPERATORS = new RegExp('([' + exports.OPERATORS.map((o) => '\\' + o).join('') + '\\(\\)])', 'g');
class ExpressionHandler {
constructor(autoClearCache, skipValidation) {
this.skipValidation = skipValidation;
this.cache = new cache_1.Cache(autoClearCache);
}
interpretExpression(expression) {
if ((0, expression_1.isNumericExpr)(expression)) {
return parseFloat(expression);
}
else if (typeof expression === 'string') {
const expressionString = expression;
return this.cache.cacheResult(expressionString, () => {
const expr = expressionString.replace(exports.REGEXP_OPERATORS, ' $1 '); // Make sure there's a space between every operator & operand
const words = (0, lib_1.compact)(expr.split(' '));
if (words.length === 0)
return null; // empty expression
// Fix special case: a + - b
for (let i = words.length - 2; i >= 1; i--) {
if ((words[i] === '-' || words[i] === '+') && wordIsOperator(exports.OPERATORS, words[i - 1])) {
words[i] = words[i] + words[i + 1];
words.splice(i + 1, 1);
}
}
const innerExpression = this.wrapInnerExpressions(words);
if (innerExpression.rest.length)
throw new Error(`interpretExpression: syntax error: parentheses don't add up in "${expr}".`);
if (innerExpression.inner.length % 2 !== 1) {
throw new Error(`interpretExpression: operands & operators don't add up: "${innerExpression.inner.join(' ')}".`);
}
const returnExpression = this.words2Expression(exports.OPERATORS, innerExpression.inner);
if (!this.skipValidation)
this.validateExpression(exports.OPERATORS, returnExpression);
return returnExpression;
}, 60 * 60 * 1000 // 1 hour
);
}
else {
return expression;
}
}
/** Try to simplify an expression, this includes:
* * Combine constant operands, using arithmetic operators
* ...more to come?
*/
simplifyExpression(expr0) {
const expr = typeof expr0 === 'string' ? this.interpretExpression(expr0) : expr0;
if (!expr)
return expr;
if (isExpressionObject(expr)) {
const l = this.simplifyExpression(expr.l);
const o = expr.o;
const r = this.simplifyExpression(expr.r);
if (typeof l === 'number' && typeof r === 'number') {
// The operands can be combined:
switch (o) {
case '+':
return l + r;
case '-':
return l - r;
case '*':
return l * r;
case '/':
return l / r;
case '%':
return l % r;
default:
return { l, o, r };
}
}
return { l, o, r };
}
return expr;
}
// Turns ['a', '(', 'b', 'c', ')'] into ['a', ['b', 'c']]
// or ['a', '&', '!', 'b'] into ['a', '&', ['', '!', 'b']]
wrapInnerExpressions(words) {
for (let i = 0; i < words.length; i++) {
switch (words[i]) {
case '(': {
const tmp = this.wrapInnerExpressions(words.slice(i + 1));
// insert inner expression and remove tha
words[i] = tmp.inner;
words.splice(i + 1, 99999, ...tmp.rest);
break;
}
case ')':
return {
inner: words.slice(0, i),
rest: words.slice(i + 1),
};
case '!': {
const tmp = this.wrapInnerExpressions(words.slice(i + 1));
// insert inner expression after the '!'
words[i] = ['', '!'].concat(tmp.inner);
words.splice(i + 1, 99999, ...tmp.rest);
break;
}
}
}
return {
inner: words,
rest: [],
};
}
/** Validates an expression. Returns true on success, throws error if not */
validateExpression(operatorList, expr0, breadcrumbs) {
if (!breadcrumbs)
breadcrumbs = 'ROOT';
if ((0, lib_1.isObject)(expr0) && !(0, lib_1.isArray)(expr0)) {
const expr = expr0;
if (expr.l === undefined)
throw new Error(`validateExpression: ${breadcrumbs}.l missing in ${JSON.stringify(expr)}`);
if (expr.o === undefined)
throw new Error(`validateExpression: ${breadcrumbs}.o missing in ${JSON.stringify(expr)}`);
if (expr.r === undefined)
throw new Error(`validateExpression: ${breadcrumbs}.r missing in ${JSON.stringify(expr)}`);
if (typeof expr.o !== 'string')
throw new Error(`validateExpression: ${breadcrumbs}.o not a string`);
if (!wordIsOperator(operatorList, expr.o))
throw new Error(breadcrumbs + '.o not valid: "' + expr.o + '"');
return (this.validateExpression(operatorList, expr.l, breadcrumbs + '.l') &&
this.validateExpression(operatorList, expr.r, breadcrumbs + '.r'));
}
else if (expr0 !== null && typeof expr0 !== 'string' && typeof expr0 !== 'number') {
throw new Error(`validateExpression: ${breadcrumbs} is of invalid type`);
}
return true;
}
clearCache() {
this.cache.clear();
}
words2Expression(operatorList, words) {
/* istanbul ignore if */
if (!words?.length)
throw new Error('words2Expression: syntax error: unbalanced expression');
while (words.length === 1 && words[0] !== null && (0, lib_1.isArray)(words[0]))
words = words[0];
if (words.length === 1)
return words[0];
// Find the operator with the highest priority:
let operatorI = -1;
for (let i = 0; i < operatorList.length; i++) {
const operator = operatorList[i];
if (operatorI === -1) {
operatorI = words.lastIndexOf(operator);
}
}
if (operatorI !== -1) {
const l = words.slice(0, operatorI);
const r = words.slice(operatorI + 1);
const expr = {
l: this.words2Expression(operatorList, l),
o: words[operatorI],
r: this.words2Expression(operatorList, r),
};
return expr;
}
else
throw new Error('words2Expression: syntax error: operator not found: "' + words.join(' ') + '"');
}
}
exports.ExpressionHandler = ExpressionHandler;
function isExpressionObject(expr) {
return (typeof expr === 'object' &&
expr !== null &&
expr.l !== undefined &&
expr.o !== undefined &&
expr.r !== undefined);
}
function wordIsOperator(operatorList, word) {
if (operatorList.indexOf(word) !== -1)
return true;
return false;
}
//# sourceMappingURL=ExpressionHandler.js.map