imicros-feel-interpreter
Version:
DMN decisions and FEEL language interpreter
1,103 lines (1,035 loc) • 77.5 kB
JavaScript
/**
* @license MIT, imicros.de (c) 2022 Andreas Leinen
*
*/
"use strict";
const nearley = require("nearley");
const grammar = require("./feel.grammar.js");
const Node = require("./ast.js");
const { Temporal, DateAndTime, DateOnly, Time, Duration} = require("./datetime.js");
const { Strings } = require("./strings.js");
const Decimal = require('decimal.js');
const Composer = require("./composer.js").Composer;
const _ = require("./lodash");
const util = require('util');
class InterpreterError extends Error {
constructor(e, { node, error }) {
super(e);
Error.captureStackTrace(this, this.constructor);
this.message = e.message || e;
this.name = this.constructor.name;
this.node = node;
this.error = error;
}
}
class ParserError extends Error {
constructor(e, { text, position, offset, line, col, original }) {
super(e);
Error.captureStackTrace(this, this.constructor);
this.message = ( e.message || e ) + " at position '" + position + "' (line " + line + ", col " + col + ")";
this.name = this.constructor.name;
this.text = text;
this.position = position;
this.offset = offset;
this.line = line;
this.col = col;
this.error = e;
this.original = original;
}
}
class Logger {
constructor () {
this.log = [];
this.limit = 50000;
}
activate () {
this.active = true;
}
deactivate () {
this.active = false;
}
clear () {
this.log = [];
}
add (entry) {
if (this.active && this.log.length < this.limit) this.log.push(entry);
}
getLog () {
return this.log;
}
}
class Interpreter {
constructor () {
this.logger = new Logger();
}
parse (exp) {
const parser = new nearley.Parser(nearley.Grammar.fromCompiled(grammar));
try {
this.error = null;
parser.feed(exp);
this.ast = parser.results;
this.exp = exp;
return true;
} catch(e) {
this.ast = {};
this.error = {
text: e.token?.text || null,
position: exp.substring(e.token?.offset > 10 ? e.token?.offset - 100 : 0, e.token?.offset || 0),
offset: e.token?.offset || null,
line: e.token?.line || null,
col: e.token?.col || null,
original: e
};
throw new ParserError("Parsing failed",this.error);
}
}
evaluate ({ expression, context } = {}) {
this.error = null;
this.composer = new Composer();
if (expression || context) {
if (expression) this.parse(expression);
this.data = context || {};
} else {
if (arguments.length > 1) {
this.parse(arguments[0]);
this.data = arguments[1];
} else if (typeof arguments[0] === "string" ) {
this.parse(arguments[0]);
this.data = {};
} else {
this.data = arguments[0];
}
}
if (this.ast && this.ast.length) {
if (!this.ast.length > 0) {
this.error = {
exp: this.exp,
number: 300 // 300 no parsing result
};
return null;
// TODO should be checked, but already as a part of parse method - currently 2 of the interpreter tests are with more than 1 result
/*
} else if (this.ast.length > 1) {
console.log(this.ast);
this.error = {
exp: this.exp,
number: 400 // 400 multiple parsing results
};
return null;
*/
}
}
if (!this.error) {
this.decisionContext = {};
this.functionDefinitions = {};
try {
const result = this._build(this.ast[0], this.data);
return result && result instanceof Temporal ? result.exp : result;
} catch(e) {
this.error = {
exp: this.exp,
number: 200, // 200 evaluation error
message: e.message
};
throw e;
}
}
return null;
}
getAst() {
return this.ast;
}
setAst(ast) {
this.ast = ast;
}
_build (node,context) {
if (!node) return "#undefined";
/* istanbul ignore else */
if (this["__"+node.node] && {}.toString.call(this["__"+node.node]) === "[object Function]") {
return this["__"+node.node](node,context);
} else {
console.log("_build",util.inspect(node, { showHidden: false, depth: null, colors: true }));
throw new Error("Interpreter - missing function " + node.node);
}
}
__NUMBER (node) {
return node.float;
}
__NAME (node,context) {
if (context && node.value in context) return context[node.value];
if (this.decisionContext && node.value in this.decisionContext) return this.decisionContext[node.value];
return this.data[node.value];
}
__BOOLEAN (node) {
return node.value;
}
__STRING (node) {
return node.value.replace(/^"(.+)"$/,'$1');
}
__NULL (node) {
return null;
}
__DASH (node) {
return true;
}
__SUM (node,context) {
let left = this._build(node.left,context);
const right = this._build(node.right,context);
if (left instanceof Temporal) {
switch (node.operator) {
case "-": return left.subtract(right);
case "+": return left.add(right);
}
}
switch (node.operator) {
case "-": return typeof left === 'number' && typeof right === 'number' ? new Decimal(left).minus(right).toNumber() : left - right;
case "+": return typeof left === 'number' && typeof right === 'number' ? new Decimal(left).plus(right).toNumber() : left + right;
}
}
__PRODUCT (node,context) {
try {
switch (node.operator) {
case "/": return new Decimal(this._build(node.left,context) || NaN).div(new Decimal(this._build(node.right,context)) || NaN).toNumber(); break;
case "*": return new Decimal(this._build(node.left,context) || NaN).mul(new Decimal(this._build(node.right,context) || NaN)).toNumber(); break;
}
} catch (e) {
throw new InterpreterError(`Failed evaluation of product ${node.operator}`, { node, context, error: e });
}
}
__EXPONENTATION (node,context) {
return Decimal.pow(new Decimal(this._build(node.left,context)),new Decimal(this._build(node.right,context))).toNumber();
}
__NEGATION (node,context) {
return -(this._build(node.expression,context));
}
__COMPARISON (node,context) {
let left = this._build(node.left,context);
let right = this._build(node.right,context);
if (left instanceof Temporal) left = left.value;
if (right instanceof Temporal) right = right.value;
// if (context?._debug) console.log(node.left, context, right);
switch (node.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;
}
}
__LOGICAL (node,context) {
switch (node.operator) {
case "and": return this._build(node.left,context) && this._build(node.right,context);
case "or": return this._build(node.left,context) || this._build(node.right,context);
}
}
__NOT (node,context) {
return !(this._build(node.parameters,context));
}
__EVAL (node,context) {
return this._build(node.expression,context);
}
__LIST (node,context) {
let list = [];
if (Array.isArray(node.entries)) {
node.entries.forEach((entry) => {
list.push(this._build(entry,context));
});
}
return list;
}
__PATH (node, context) {
let obj = this._build(node.object, context);
if (Array.isArray(obj)) {
let projection = [];
obj.forEach((entry) => {
projection.push(entry && node.property && node.property.value ? entry[node.property.value] : null);
});
return projection;
}
return obj && node.property && node.property.value ? ( obj[node.property.value] === undefined ? null : obj[node.property.value] ) : null;
}
__IF (node) {
return this._build(node.condition) ? this._build(node.then) : this._build(node.else);
}
__CONTEXT (node,context) {
// console.log("_CONTEXT",util.inspect(context, { showHidden: false, depth: null, colors: true }));
let result = {};
if (node.data && Array.isArray(node.data.entries)) {
node.data.entries.forEach((entry) => {
let element = this._build(entry,context);
if (element) {
// normalize spaces in key
element.key = element.key.replace(/\s\s+/g, ' ');
result[element.key] = element.value;
// store for use in following expressions
this.decisionContext[element.key] = element.value;
}
});
}
this.logger.add({
type: "Context",
value: result
})
return result;
}
__CONTEXT_ENTRY (node,context) {
try {
// console.log("_CONTEXT_ENTRY",util.inspect(context, { showHidden: false, depth: null, colors: true }));
if (node.expression.node === Node.FUNCTION_DEFINITION) {
const name = node.key.node === Node.NAME ? node.key.value : this._build(node.key,context);
this.functionDefinitions[name] = node.expression;
return null;
}
let result = { key: node.key.node === Node.NAME ? node.key.value : this._build(node.key,context), value: this._build(node.expression,context) };
this.logger.add({ type: "Context entry", ...result });
return result;
} catch (e) {
throw new InterpreterError(`Failed evaluation of context entry`, { node, context, error: e });
}
}
__DATE_AND_TIME (node,context) {
// console.log("_DATE_AND_TIME",util.inspect(context, { showHidden: false, depth: null, colors: true }));
let parameters = this._build(node.parameters,context);
// console.log("_DATE_AND_TIME:parameters",util.inspect(node.parameters, { showHidden: false, depth: null, colors: true }));
if (Array.isArray(parameters) && parameters.length === 3 && node.name === 'date') return DateOnly.build(parameters);
if (Array.isArray(parameters) && parameters.length >= 3 && node.name === 'time') return Time.build(parameters);
return Temporal.parse(parameters ? parameters[0] : null);
}
__AT_LITERAL (node,context) {
let expression = this._build(node.expression,context);
return Temporal.parse(expression ? expression : null);
}
__FILTER (node) {
let list = this._build(node.list);
if (!Array.isArray(list)) return null;
switch (node.filter.node) {
case Node.NUMBER:
let i = this._build(node.filter);
return i > 0 ? (i <= list.length ? list[i-1] : null) : null;
case Node.NEGATION:
let n = this._build(node.filter);
return list.length + n > 0 ? list[list.length + n-1] : null;
case Node.NAME:
let m = this._build(node.filter);
return m >= 0 ? (m <= list.length ? list[m-1] : null) : (list.length + m > 0 ? list[list.length + m-1] : null);
case Node.COMPARISON:
let filtered = [];
// filter by key
if (node.filter.operator === "=" && node.filter.left.node === Node.NAME && node.filter.left.value.toUpperCase() === "KEY") {
let key = this._build(node.filter.right);
return list.find((item) => { return typeof item === 'object' && item.key === key; });
}
list.forEach((item) => {
if (this._build(node.filter, { ...item, item }) === true) filtered.push(item);
})
return filtered;
default:
let r = [];
list.forEach((item) => {
if (this._build(node.filter, { ...item, item }) == true) r.push(item);
})
return r;
}
}
__FOR (node,context) {
let list = this._build(node.context);
const key = node.var?.value || null;
if (!Array.isArray(list)) return null;
let r = [];
list.forEach((item) => {
const local = {};
local[key] = item;
r.push(this._build(node.return, Object.assign(context || {},local)));
})
return r;
}
__IN (node,context) {
switch (node.test.node) {
case Node.DASH: return true;
case Node.EVAL:
if (node.test.expression.node === Node.UNARY) {
node.test.expression.input = node.input;
return this._build(node.test.expression,context);
}
if (node.test.expression.node === Node.INTERVAL) {
let testNode = new Node({ node: Node.IN, input: node.input, test: node.test.expression });
return this._build(testNode,context);
}
break;
case Node.NAME:
case Node.STRING:
case Node.BOOLEAN:
case Node.NUMBER:
case Node.FUNCTION_CALL:
return (this._build(node.input,context) === this._build(node.test,context));
case Node.INTERVAL:
let lower = false, upper = false;
let input = this._build(node.input,context);
if (input instanceof Temporal) input = input.value;
let from = this._build(node.test.from,context);
let to = this._build(node.test.to,context);
if (from instanceof Temporal) from = from.value;
if (to instanceof Temporal) to = to.value;
switch (node.test.open) {
case "[": lower = input >= from; break;
case "(":
case "]": lower = input > from; break;
}
switch (node.test.close) {
case "]": upper = input <= to; break;
case ")":
case "[": upper = input < to; break;
}
return lower && upper;
case Node.UNARY:
node.test.input = node.input;
return this._build(node.test,context);
case Node.UNARYTESTS:
let result = false;
// unary tests like >8, <10, >=8, <=10, =8, !=8, "any", "list" - just one of them must evaluate to true
node.test.list.entries.forEach((test) => {
let testNode = new Node({ node: Node.IN, input: node.input, test });
// if (this.logger.active) console.log("_UNARYTEST",util.inspect({ testNode, result: this._build(testNode,context) }, { showHidden: false, depth: null, colors: true }));
result = result || this._build(testNode,context);
})
return result;
default:
return false;
}
}
__BETWEEN (node,context) {
let test = this._build(node.expression,context);
if (test instanceof Temporal) test = test.value;
let lower = this._build(node.left,context);
if (lower instanceof Temporal) lower = lower.value;
let upper = this._build(node.right,context);
if (upper instanceof Temporal) upper = upper.value;
// console.log({test,lower,upper});
return test > lower && test < upper;
}
__IN_LIST (node,context) {
if (!node.list || !Array.isArray(node.list.entries)) return false;
let result = false;
node.list.entries.forEach((entry) => {
switch (entry.node) {
case Node.UNARY:
entry.input = node.input;
result = result || this._build(entry,context);
break;
default:
result = result || (this._build(node.input,context) === this._build(entry,context));
}
});
return result;
}
__UNARY (node,context) {
if (!node.input) return false;
switch (node.operator) {
case "!=": return this._build(node.input,context) !== this._build(node.value,context);
case "<": return this._build(node.input,context) < this._build(node.value,context);
case ">": return this._build(node.input,context) > this._build(node.value,context);
case ">=": return this._build(node.input,context) >= this._build(node.value,context);
case "<=": return this._build(node.input,context) <= this._build(node.value,context);
}
}
__INSTANCE_OF (node,context) {
let instance = this._build(node.instance,context);
// a instance of b
if (node.of.node && node.of.node == Node.NAME) return typeof instance == typeof this._build(node.of,context);
// TODO a instance of list<..>
// TODO a instance of range<..>
// TODO a instance of date|time|date and time|day-time-duration|year-month-duration
// TODO a instance of context|function
// a instance of number|string|boolean
if (typeof node.of == 'string') {
switch (node.of) {
case "number": return typeof instance == 'number';
case "string": return typeof instance == 'string';
case "boolean": return typeof instance == 'boolean';
case "date": return instance instanceof DateOnly;
case "date and time": return instance instanceof DateAndTime;
default: return false;
}
}
}
__BOXED (node,context) {
return this._build(node.result,Object.assign(context || {},this._build(node.context) || {}));
}
// DMN Main
__MAIN (node,context) {
// clone context
let localContext = structuredClone(context);
// console.log("_MAIN",util.inspect(node, { showHidden: false, depth: null, colors: true }));
let result = null;
for (let i = 0; i < node.definitions.length; i++) {
switch (node.definitions[i].node) {
case Node.INPUT: result = this._build(node.definitions[i],localContext); result = localContext; break;
case Node.RETURN: result = this._build(node.definitions[i],localContext); break;
case Node.DECISIONTABLE: result = this._build(node.definitions[i],localContext); break;
}
}
return result;
}
// DMN Input Expression
__INPUT (node,context) {
let result = {};
context[node.name] = this._build(node.value,context);
result[node.name] = context[node.name];
this.logger.add({
type: "Input",
name: node.name,
value: context[node.name]
});
return result;
}
// DMN Rule
__RULE (node,context) {
let steps = [];
for (let i = 0; i < node.inputs.length; i++) {
steps.push({
name: node.inputs[i].name,
value: this._build(node.inputs[i].value,context),
expression: node.inputs[i].expression,
//ast: node.inputs[i].test,
result: this._build(node.inputs[i].test,context)
});
};
let rule = {
result: true,
output: {}
};
steps.forEach(step => { rule.result = rule.result && step.result; });
let outputs = [];
if (rule.result) {
for (let i = 0; i < node.outputs.length; i++) {
let value = node.outputs[i].value ? this._build(node.outputs[i].value,context) : null
_.set(rule.output,node.outputs[i].name,value,context);
}
}
this.logger.add({
type: "Rule",
decisionTable: node.decisionTable,
index: node.index,
annotation: node.annotation,
steps,
...rule
});
return rule;
}
// DMN Decision Table
__DECISIONTABLE (node,context) {
// build inputs
let inputs = [];
for (let i = 0; i < node.inputs.length; i++) {
inputs.push({
name: node.inputs[i].name,
value: this._build(node.inputs[i].value,context)
});
}
// evaluate rules
let results = [];
for (let i = 0; i < node.rules.length; i++) {
results.push(this._build(node.rules[i],context));
}
// evaluate rule results according to hit policy
let output = {};
// Hit policy
switch (node.hitPolicy) {
// Unique: no overlap is possible and all rules are disjoint. Only a single rule can be matched.
case "U":
case "Unique": {
results.some((rule) => {
if (rule.result) {
output = rule.output;
context = Object.assign(context,rule.output);
return true;
} else {
return false;
}
});
break;
}
// Any: there may be overlap, but all the matching rules show equal output entries for each output (ignoring rule
// annotations), so any match can be used. If the output entries are non-equal (ignoring rule annotations), the
// hit policy is incorrect and the result is undefined.
// currently handled as unique
case "A":
case "Any": {
results.some((rule) => {
if (rule.result) {
output = rule.output;
context = Object.assign(context,rule.output);
return true;
} else {
return false;
}
});
break;
}
// First: multiple (overlapping) rules can match, with different output entries. The first hit by rule order is returned
// (and evaluation can halt). This is still a common usage, because it resolves inconsistencies by forcing the
// first hit. However, first hit tables are not considered good practice because they do not offer a clear
// overview of the decision logic. It is important to distinguish this type of table from others because the
// meaning depends on the order of the rules. The last rule is often the catch-remainder. Because of this order,
// the table is hard to validate manually and therefore has to be used with care.
// currently handled as unique
case "F":
case "First": {
results.some((rule) => {
if (rule.result) {
output = rule.output;
context = Object.assign(context,rule.output);
return true;
} else {
return false;
}
});
break;
}
// Rule order: returns all hits in rule order. Note: the meaning may depend on the sequence of the rules.
case "R":
case "Rule order": {
results.forEach((rule) => {
if (rule.result) {
output.push(rule.output);
}
});
break;
}
// Collect: returns all hits in arbitrary order.
case "C":
case "Collect": {
switch (node.aggregation) {
case "SUM": {
results.forEach((rule) => {
if (rule.result) {
for (let key in rule.output) {
output[key] ? output[key] += rule.output[key] : output[key] = rule.output[key];
}
}
});
break;
}
case "MIN": {
results.forEach((rule) => {
if (rule.result) {
for (let key in rule.output) {
if (!output[key] || rule.output[key] < output[key]) output[key] = rule.output[key];
}
}
});
break;
}
case "MAX": {
results.forEach((rule) => {
if (rule.result) {
for (let key in rule.output) {
if (!output[key] || rule.output[key] > output[key]) output[key] = rule.output[key];
}
}
});
break;
}
case "COUNT": {
results.forEach((rule) => {
if (rule.result) {
for (let key in rule.output) {
output[key] ? output[key]++ : output[key] = 1;
}
}
});
break;
}
default: {
results.forEach((rule) => {
if (rule.result) {
// merge outputs
for (let key in rule.output) {
output[key] ? output[key].push(rule.output[key]) : output[key]= [rule.output[key]];
}
}
});
}
};
break;
}
/*
// Collect + (sum): the result of the decision table is the sum of all the outputs
case "C+": {
final = {};
if (parameters["outputs"].length !== 1) return undefined;
const key = this._build(parameters["outputs"][0],context);;
results.forEach((result) => {
if (result.result) !final[key] ? final[key] = result.output[key] : final[key] += result.output[key];
});
return final;
}
// Collect < (min): the result of the decision table is the smallest value of all the outputs
case "C<": {
final = {};
if (parameters["outputs"].length !== 1) return undefined;
const key = this._build(parameters["outputs"][0],context);;
results.forEach((result) => {
if (result.result && (!final[key] || result.output[key] < final[key])) final[key] = result.output[key];
});
return final;
}
// Collect > (max): the result of the decision table is the largest value of all the outputs
case "C>": {
final = {};
if (parameters["outputs"].length !== 1) return undefined;
const key = this._build(parameters["outputs"][0],context);;
results.forEach((result) => {
if (result.result && (!final[key] || result.output[key] > final[key])) final[key] = result.output[key];
});
return final;
}
// Collect # (count): the result of the decision table is the number of outputs
case "C#": {
final = {};
if (parameters["outputs"].length !== 1) return undefined;
const key = this._build(parameters["outputs"][0],context);;
final[key] = 0;
results.forEach((result) => {
if (result.result) final[key]++;
});
return final;
}
*/
}
this.logger.add({
type : "Decisiontable",
name : node.name,
hitPolicy : node.hitPolicy,
inputs : inputs,
output
});
// default: return empty object
return output;
}
// DMN RETURN
__RETURN (node,context) {
let result = {};
result[node.name] = this._build(node.value,context);
context[node.name] = result[node.name];
this.logger.add({
type: "Return",
name: node.name,
expression: node.expression,
value: result[node.name]
});
return result;
}
buildParameters (node,names,context) {
let parameters = {};
if (node && node.node === Node.LIST) {
node.entries.forEach((entry,index) => {
if (entry.node == Node.NAMED_PARAMETER) {
// named parameters
let name = entry.name?.value ?? null;
if (names.indexOf(name) >= 0) parameters[name] = this._build(entry.expression,context);
} else {
// positional parameters
parameters[names[index]] = this._build(entry,context);
}
})
}
return parameters;
}
getRawParameters (node,names) {
let parameters = {};
if (node && node.node === Node.LIST) {
node.entries.forEach((entry,index) => {
if (entry.node == Node.NAMED_PARAMETER) {
// named parameters
let name = entry.name?.value ?? null;
if (names.indexOf(name) >= 0) parameters[name] = entry.expression;
} else {
// positional parameters
parameters[names[index]] = entry;
}
})
}
return parameters;
}
getRangeParameters (entries,context) {
const compare = Object.entries(entries).map(([key,value]) => {
let parameter = {
raw: value,
type: value?.node === Node.INTERVAL ? "interval" : "point"
};
if (parameter.type === "point") {
parameter.value = this._build(parameter.raw,context);
if (parameter.value instanceof Temporal) parameter.value = parameter.value.value
} else {
parameter.from = this._build(value.from,context);
if (parameter.from instanceof Temporal) parameter.from = parameter.from.value
parameter.to = this._build(value.to,context);
if (parameter.to instanceof Temporal) parameter.to = parameter.to.value
parameter.includeFrom = value.open === "[" ? true : false;
parameter.includeTo = value.close === "]" ? true : false;
}
return parameter;
})
return compare;
}
callFunctionDefinition (definition,parameters,context) {
// build formal paramater list
const parameterList = definition.parameters?.entries?.map(entry => entry.name?.value ?? null);
// map parameters to build local context
const localContext = this.buildParameters(parameters,parameterList,context);
// call function defintion and return result
// TODO: shouldn't we restrict her to the use of the local context only? Couldn't find any explanation about the valid scope in the DMN notation
return this._build(definition.expression,Object.assign(context || {}, localContext));
}
__FUNCTION_CALL (node,context) {
// console.log("_FUNCTION_CALL",util.inspect(context, { showHidden: false, depth: null, colors: true }));
// defined functions
if (this.functionDefinitions[node.name.value]) {
return this.callFunctionDefinition(this.functionDefinitions[node.name.value],node.parameters,context);
}
// build-in functions
switch (node.name.value) {
case "today": return Temporal.today();
case "now": return Temporal.now();
case "day of week": {
const parameters = this.buildParameters(node.parameters,["date"],context);
return Temporal.dayOfWeek(parameters);
}
case "day of year": {
const parameters = this.buildParameters(node.parameters,["date"],context);
return Temporal.dayOfYear(parameters);
}
case "week of year": {
const parameters = this.buildParameters(node.parameters,["date"],context);
return Temporal.weekOfYear(parameters);
}
case "month of year": {
const parameters = this.buildParameters(node.parameters,["date"],context);
return Temporal.monthOfYear(parameters);
}
case "years and months duration": {
const parameters = this.buildParameters(node.parameters,["from","to"],context);
return Temporal.monthBetween(parameters);
}
case "number": {
const parameters = this.buildParameters(node.parameters,["from"],context);
return typeof parameters.from == 'string' ? parseFloat(parameters.from) : undefined;
}
case "string": {
const parameters = this.buildParameters(node.parameters,["from"],context);
if (parameters.from instanceof Temporal) return parameters.from.exp;
return String(parameters.from);
}
case "context": {
const parameters = this.buildParameters(node.parameters,["entries"],context);
return Array.isArray(parameters.entries) ? parameters.entries.reduce((prev,curr) => { if (curr.key) prev[curr.key] = curr.value; return prev; },{}) : undefined;
}
case "is defined": {
const parameters = this.buildParameters(node.parameters,["value"],context);
return parameters.value === undefined || parameters.value === null ? false : true;
}
case "substring": {
const parameters = this.buildParameters(node.parameters,["string","start","length"],context);
return (typeof parameters.string == 'string') ? parameters.string.substring(parameters.start-1, parameters.length ? parameters.start+parameters.length-1 : parameters.string.length) : undefined;
}
case "string length": {
const parameters = this.buildParameters(node.parameters,["string"],context);
return (typeof parameters.string == 'string') ? parameters.string.length : undefined;
}
case "upper case": {
const parameters = this.buildParameters(node.parameters,["string"],context);
return (typeof parameters.string == 'string') ? parameters.string.toUpperCase() : undefined;
}
case "lower case": {
const parameters = this.buildParameters(node.parameters,["string"],context);
return (typeof parameters.string == 'string') ? parameters.string.toLowerCase() : undefined;
}
case "substring before": {
const parameters = this.buildParameters(node.parameters,["string","match"],context);
return (typeof parameters.string == 'string') ? parameters.string.substring(0, parameters.string.indexOf(parameters.match)) : undefined;
}
case "substring after": {
const parameters = this.buildParameters(node.parameters,["string","match"],context);
return (typeof parameters.string == 'string') ? parameters.string.substring(parameters.string.indexOf(parameters.match)+parameters.match.length,parameters.string.length) : undefined;
}
case "contains": {
const parameters = this.buildParameters(node.parameters,["string","match"],context);
return (typeof parameters.string == 'string') ? ( parameters.string.indexOf(parameters.match) >= 0 ? true : false ) : undefined;
}
case "starts with": {
const parameters = this.buildParameters(node.parameters,["string","match"],context);
return (typeof parameters.string == 'string') ? parameters.string.startsWith(parameters.match) : undefined;
}
case "ends with": {
const parameters = this.buildParameters(node.parameters,["string","match"],context);
return (typeof parameters.string == 'string') ? parameters.string.endsWith(parameters.match) : undefined;
}
case "matches": {
const parameters = this.buildParameters(node.parameters,["input","pattern"],context);
return Strings.matches(parameters);
}
case "replace": {
const parameters = this.buildParameters(node.parameters,["input","pattern","replacement","flags"],context);
return Strings.replace(parameters);
}
case "split": {
const parameters = this.buildParameters(node.parameters,["string","delimiter"],context);
return Strings.split(parameters);
}
case "extract": {
const parameters = this.buildParameters(node.parameters,["string","pattern"],context);
return Strings.extract(parameters);
}
case "list contains": {
const parameters = this.buildParameters(node.parameters,["list","element"],context);
return (Array.isArray(parameters.list)) ? parameters.list.indexOf(parameters.element) >= 0 : false;
}
case "count": {
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? parameters.list.length : undefined;
}
case "min": {
if (node.parameters?.entries?.length > 1) return Math.min(...this._build(node.parameters));
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? Math.min(...parameters.list) : undefined;
}
case "max": {
if (node.parameters?.entries?.length > 1) return Math.max(...this._build(node.parameters));
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? Math.max(...parameters.list) : undefined;
}
case "sum": {
if (node.parameters?.entries?.length > 1) return this._build(node.parameters).reduce((a,b)=>a+b);
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? parameters.list.reduce((a,b)=>a+b,0) : undefined;
}
case "product": {
if (node.parameters?.entries?.length > 1) return this._build(node.parameters).reduce((a,b)=>a*b);
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? ( parameters.list.length > 0 ? parameters.list.reduce((a,b)=>a*b) : 0 ) : undefined;
}
case "mean": {
if (node.parameters?.entries?.length > 1) return (this._build(node.parameters).reduce((a,b)=>a+b) / node.parameters.entries.length) || 0;
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? ( parameters.list.length > 0 ? (parameters.list.reduce((a,b)=>a+b) / parameters.list.length) : 0 ) : undefined;
}
case "median": {
const parameters = node.parameters?.entries?.length > 1 ? [...this._build(node.parameters)] : this.buildParameters(node.parameters,["list"],context).list;
function median(values) {
values.sort(function(a,b){
return a-b;
});
var half = Math.floor(values.length / 2);
if (values.length % 2)
return values[half];
return (values[half - 1] + values[half]) / 2.0;
}
return (Array.isArray(parameters) && parameters.length > 0) ? median(parameters) : null;
}
case "stddev": {
const parameters = node.parameters?.entries?.length > 1 ? [...this._build(node.parameters)] : this.buildParameters(node.parameters,["list"],context).list;
if (!Array.isArray(parameters)) return undefined;
const n = parameters.length
const mean = parameters.reduce((a, b) => a + b) / n
return Math.sqrt(parameters.map(x => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n);
}
case "mode": {
const parameters = node.parameters?.entries?.length > 1 ? [...this._build(node.parameters)] : this.buildParameters(node.parameters,["list"],context).list;
function getModes(array) {
let unique = []; // array of unique values.
let count = []; // array of frequency
let maxFreq = 0; // holds the max frequency.
let modes = [];
array.forEach((item,index) => {
let n = unique.indexOf(item);
if (n < 0) {
unique.push(item);
n = unique.length - 1;
}
count[n] ? count[n]++ : count[n] = 1;
if (count[n] > maxFreq) maxFreq = count[n];
})
for (let k in count) {
if (count[k] == maxFreq) {
modes.push(unique[k]);
}
}
return modes.sort();
}
return Array.isArray(parameters) ? getModes(parameters) : null;
}
case "all":
case "and": {
if (node.parameters?.entries?.length > 1) return this._build(node.parameters).reduce((a,b)=> a && b);
const parameters = this.buildParameters(node.parameters,["list"],context);
// console.log(util.inspect(parameters, { showHidden: false, depth: null, colors: true }));
return (Array.isArray(parameters.list)) ? (parameters.list.length > 0 ? parameters.list.reduce((a,b) => a && b) : true ) : undefined;
}
case "any":
case "or": {
if (node.parameters?.entries?.length > 1) return this._build(node.parameters).reduce((a,b)=> a || b);
const parameters = this.buildParameters(node.parameters,["list"],context);
// console.log(util.inspect(parameters, { showHidden: false, depth: null, colors: true }));
return (Array.isArray(parameters.list)) ? (parameters.list.length > 0 ? parameters.list.reduce((a,b) => a || b) : false ) : undefined;
}
case "sublist": {
const parameters = this.buildParameters(node.parameters,["list","start position","length"],context);
if (!Array.isArray(parameters.list)) return undefined;
const start = parameters["start position"] && parameters["start position"] > 0 ? parameters["start position"] - 1 : 0;
const end = parameters.length && parameters.length > 0 ? start + parameters.length : parameters.list.length;
return parameters.list.slice(start,end);
}
case "append": {
const parameters = this.buildParameters(node.parameters,["list"],context);
const elements = node.parameters?.entries?.length > 1 ? this._build(node.parameters).slice(1) : [];
return (Array.isArray(parameters.list)) ? parameters.list.concat(elements) : undefined;
}
case "union":
case "concatenate": {
// console.log(util.inspect(node.parameters, { showHidden: false, depth: null, colors: true }));
const elements = node.parameters?.entries?.length > 1 ? this._build(node.parameters) : [];
return elements.reduce((a,b) => { return Array.isArray(b) ? ( Array.isArray(a) ? a.concat(b) : [].concat(b) ) : a; });
}
case "insert before": {
const parameters = this.buildParameters(node.parameters,["list","position"],context);
const elements = node.parameters?.entries?.length > 2 ? this._build(node.parameters).slice(2) : [];
Array.isArray(parameters.list) && parameters.position > 0 ? parameters.list.splice(parameters.position - 1, 0, ...elements) : parameters.list = undefined;
return parameters.list;
}
case "remove": {
const parameters = this.buildParameters(node.parameters,["list","position"],context);
Array.isArray(parameters.list) && parameters.position > 0 ? parameters.list.splice(parameters.position - 1, 1) : parameters.list = undefined;
return parameters.list;
}
case "reverse": {
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? parameters.list.reverse() : undefined;
}
case "index of": {
function getAllIndexes(arr, val) {
var indexes = [], i;
for(i = 0; i < arr.length; i++)
if (arr[i] === val)
indexes.push(i+1);
return indexes;
}
const parameters = this.buildParameters(node.parameters,["list","match"],context);
return (Array.isArray(parameters.list)) ? getAllIndexes(parameters.list,parameters.match) : undefined;
}
case "distinct values": {
const parameters = this.buildParameters(node.parameters,["list"],context);
return (Array.isArray(parameters.list)) ? [...new Set(parameters.list)] : undefined;
}
case "flatten": {
const parameters = this.buildParameters(node.parameters,["list"],context);
function flattenDeep(arr1) {
return arr1.reduce((acc, val) => Array.isArray(val) ? acc.concat(flattenDeep(val)) : acc.concat(val), []);
}
return (Array.isArray(parameters.list)) ? flattenDeep(parameters.list) : undefined;
}
case "sort": {
const parameters = this.getRawParameters(node.parameters,["list","precedes"]);
var list = this._build(parameters.list,context);
const functionParameter = parameters.precedes?.paramete