imicros-feel-interpreter
v0.0.13
Published
FEEL interpreter
Downloads
31
Readme
imicros-feel-interpreter
FEEL interpreter written in JavaScript.
Developed for of the imicros backend, but can be used also stand-alone.
Installation
$ npm install imicros-feel-interpreter
Usage
const { Interpreter } = require("imicros-feel-interpreter");
const interpreter = new Interpreter();
/*** parse and evaluate in a single step ***/
let result = interpreter.evaluate("a/b**-c-d",{a:1,b:2,c:4,d:3});
// 13
/*** or in two steps: parse single evaluate multiple***/
let success = interpreter.parse("a/b**-c-d");
// true
let serialized = JSON.stringify(interpreter.ast);
interpreter.ast = JSON.parse(serialized);
// serialized ast can be stored somewhere and restored for multiple usage with different data sets
let result = interpreter.evaluate({a:1,b:2,c:4,d:3});
// 13
Usage Converter to convert a DMN file (XML) to a single FEEL expression
const { DMNParser, DMNConverter } = require("imicros-feel-interpreter");
const fs = require("fs");
const xmlData = fs.readFileSync(./assets/Sample.dmn).toString();
const expression = new DMNConverter().convert({ xml: xmlData });
Features
- Complete support of DMN 1.4. Known restrictions see below.
- Provide build-in functions as listed below.
Restrictions
- Additional name symbols ./-+* according rule 30. of the sepcification as well as keywords for,return,if,true,false,in,and,or,between,some,every,then,else,not,string,number,boolean,null,date,time,duration in names are not supported. (The package uses nearley as parser and I didn't found a way to implement the ambiguity). White spaces are allowed and normalized (doubled spaces will be replaced by just one space). Therefore expresssions like {"new example": 5}.new example as well as { "new example": 5}.new example will work. Beside white spaces the special characters _?' which are not used as operators are allowed.
- No external functions are supported.
Performance considerations
In case of intensive usage with large number of data sets consider the pre-parsing possibility.
A simple expression if even(i) then (i*a) else (i*b)
with parsing and evaluation in one step evaluates 2.500 data sets per second and with a single parsing you can evaluate up to 200.000 data sets per second on an average hardware with single thread processing.
Example expressions
date and time("2022-04-05T23:59:59") < date("2022-04-06")
w/o context -->true
if a>b then c+4 else d
with context{a:3,b:2,c:5.1,d:4}
-->9.1
{"Mother's finest":5, "result": 5 + Mother's finest}.result
-->10
"best of " + lower case("IMicros")
w/o context -->"best of imicros"
[{a:3,b:1},{a:4,b:2}][item.a > 3]
w/o context -->[{a:4,b:2}]
[1,2,3,4,5,6,7,8,9][a*(item+1)=6]
with context{a:2}
-->[2]
a+b > c+d
with context{a:5,b:4,c:3,d:5}
-->true
flight list[item.status = "cancelled"].flight number
with context{"flight list": [{ "flight number": 123, status: "boarding"},{ "flight number": 234, status: "cancelled"}]}
-->[234]
{calc:function (a:number,b:number) a-b, y:calc(b:c,a:d)+3}.y
with context{c:4,d:5}
-->4
deep.a.b + deep.c
with context{deep:{a:{b:3},c:2}}
-->5
{a:3}.a
w/o context -->3
extract("references are 1234, 1256, 1378", "12[0-9]*")
w/o context -->["1234","1256"]
(a+b)>(8.9) and (c+d)>(8.1)
with context{a:5,b:4,c:4,d:5}
-->true
@"2022-04-10T13:15:20" + @"P1M"
w/o context -->"2022-05-10T13:15:20"
day of year(@"2022-04-16")
w/o context -->106
@"P7M2Y" + @"P5D"
w/o context -->"P5D7M2Y"
{ "PMT": function (p:number,r:number,n:number) (p*r/12)/(1-(1+r/12)**-n), "MonthlyPayment": PMT(Loan.amount, Loan.rate, Loan.term) + fee }.MonthlyPayment
with context{Loan: { amount: 600000, rate: 0.0375, term:360 }, fee: 100}
-->2878.6935494327668
decision table( outputs: ["Applicant Risk Rating"], inputs: ["Applicant Age","Medical History"], rule list: [ [>60,"good","Medium"], [>60,"bad","High"], [[25..60],-,"Medium"], [<25,"good","Low"], [<25,"bad","Medium"] ], hit policy: "Unique" )
with context{"Applicant Age": 65, "Medical History": "bad"}
-->{ "Applicant Risk Rating": "High" }
Supported expressions
(not the complete list - refer to the test cases for a complete list of tested expressions)
Arithmetic
Muliplication: *, Division: /, Addition: +, Subtraction: -, Exponentation: **
(x - 2)**2 + 3/a - c*2
Negation: -
-5
Boolean
And: and, Or: or
Equal to: =, not equal to: !=, less than: <, less than or equal to: <=, greater than: >, greater than or equal to: >=
5 = 5 and 6 != 5 and 3 <= 4 and date("2022-05-08") > date("2022-05-07")
-->true
Existence check: is defined(var)
is defined({x:null}.x)
-->true
is defined({}.x)
-->false
Negation: not(expression)
{a:5,b:3,result: not(a<b)}.result
-->true
Type check: expression instance of type
a instance of b
with context{a:3,b:5}
-->true
a instance of string
with context{a:"test"}
-->true
a instance of number
with context{a:3}
-->true
a instance of boolean
with context{a:true}
-->true
String
Concatenate: + (only possible with both terms type string)
"foo" + "bar"
-->"foobar"
Context and path
Context is a defintion in JSON notation with { key: value }.
The key must evaluate to a string, the value can be any expression (including function definitions and complete decision table calls).
With the .name notation an attribute of the context is accessed.
{a:3}.a
-->3
deep.a.b + deep.c
with context{deep:{a:{b:3},c:2}}
-->5
{calc:function (a:number,b:number) a-b, y:calc(b:c,a:d)+3}.y
with context{c:4,d:5}
-->4
{calc:function (a:number,b:number) a+b, y:calc(4,5)+3}
-->{y:12}
Filter (Lists)
Get element by index (index count is starting with 1)
[1,2,3,4][2]
-->2
Negative indices are counted from the end
[1,2,3,4][-1]
-->3
[1,2,3,4][-0]
-->4
Reduce list based on logic expression - variable item is the current element
[1,2,3,4][item > 2]
-->[3,4]
[1,2,3,4,5,6,7,8,9][a*(item+1)=6]
with context{a:2}
-->[2]
[1,2,3,4][even(item)]
-->[2,4]
flight list[item.status = "cancelled"].flight number
with context{"flight list": [{ "flight number": 123, status: "boarding"},{ "flight number": 234, status: "cancelled"}]}
-->[234]
Temporal
Date or date and time expressions as well as durations can be written with the @String notation
@"2022-05-10T13:15:20" - @"P1M"
-->"2022-04-10T13:15:20"
@"13:45:20" - @"PT30M"
-->"13:15:20"
date("2022-05-14") - date("2020-09-10")
-->"P4D8M1Y"
date("2020-09-10")-date("2022-05-14")
-->"-P4D8M1Y"
Comparison with <,<=,>,>=,=
Additon/Subtraction with date|date and time +/- duration
date("2022-04-05") < date("2022-04-06")
-->true
date and time("2022-04-15T08:00:00") = date and time("2022-04-15T00:00:00") + @"P8H"
-->true
@"P5D" > @"P2D"
-->true
@"P5D" > @"P4DT23H"
-->true
Comparison with in interval
date("2022-04-05") in [date("2022-04-04")..date("2022-04-06")]
-->true
(date("2022-04-01")+duration("P3D")) in [date("2022-04-04")..date("2022-04-06")]
-->true
Comparison with between date|date and time and date|date and time
date("2022-04-05") between date("2022-04-04") and date("2022-04-06")
-->true
Access of attributes of the temporal type
@"2022-04-10".month
-->4
date("2022-04-10").day
-->10
date and time("2022-04-10T13:15:20").year
-->2022
date and time("2022-04-10T13:15:20").hour
-->13
date and time("2022-04-10T13:15:20").minute
-->15
date and time("2022-04-10T13:15:20").second
-->20
@"P12D5M".months
-->5
today().year
--> current yearnow().minute
--> current minuteday of week(@"2022-04-16")
-->"Saturday"
day of year(@"2022-04-16")
-->106
week of year(@"2022-04-16")
-->15
abs(@"-P7M2Y")
-->"P7M2Y"
If
if condition then expression else expression
if 1 > 2 then 3 else 4
For
for name in iteration context return expression
for a in [1,2,3] return a*2
-->[2,4,6]
Comments
single line comments starting with //
until the end of the line
single line or multiline comments framed with /*
and */
.
/* start
comment */
decision table(
outputs: ["Applicant Risk Rating"],
inputs: ["Applicant Age","Medical History"],
/* multi line
between */
rule list: [
[>60,"good","Medium"],
[>60,"bad","High"],
[[25..60],-,"Medium"],
/****
* important comment
****/
[<25,"good","Low"],
[<25,"bad","Medium"] // single line comment
],
hit policy: "Unique"
) /* end comment */
Supported build-in functions
Conversion
date(from|year,month,day)
time(from|hour,minute,second,offset?)
with offset type duration (e.g. @"PT1H")- missing: date and time(from - with named parameter|date,time)
years and months duration(from,to)
with from,to type datenumber(from)
with from type stringstring(from)
context(entries)
with entries type object with attributes key and value (e.g. {key: "a",value: 1})
Temporal
today()
now()
day of week(date)
day of year(date)
week of year(date)
month of year(date)
abs(duration)
Arithmetic
decimal(n,scale)
floor(n)
ceiling(n)
round up(n,scale?)
round down(n,scale?)
round half up(n,scale?)
round half down(n,scale?)
abs(number)
modulo(dividend,divisor)
sqrt(number)
log(number)
exp(number)
odd(number)
even(number)
Logical
is defined(value)
not(negand)
Ranges
before(a,b)
with a,b either point or intervalafter(a,b)
with a,b either point or intervalmeets(a,b)
with a,b intervalsmet by(a,b)
with a,b intervalsoverlaps(a,b)
with a,b intervalsoverlaps before(a,b)
with a,b intervalsoverlaps after(a,b)
with a,b intervalsfinishes(a,b)
with a eiter point or interval and b intervalfinished by(a,b)
with a interval and b either point or intervalincludes(a,b)
with a interval and b either point or intervalduring(a,b)
with a eiter point or interval and b intervalstarts(a,b)
with a eiter point or interval and b intervalstarted by(a,b)
with a interval and b either point or intervalcoinsides(a,b)
with a,b either both points or both intervals
Lists
list contains(list,element)
count(list) / count(...item)
min(list) / min(...item)
max(list) / max(...item)
sum(list) / sum(...item)
product(list) / product(...item)
mean(list) / mean(...item)
median(list) / median(...item)
stddev(list) / stddev(...item)
mode(list) / mode(...item)
all(list) / all(...item)
and(list)
any(list) / any(...item)
or(list)
sublist(list, startposition, length?)
append(list,...item)
union(...list)
concatenate(...list)
insert before(list,position,newItem)
remove(list,position)
reverse(list)
index of(list,match)
distinct values(list)
flatten(list)
sort(list,precedes)
string join(list,delimiter?,prefix?,suffix?)
Strings
substring(string,start,length)
string length(string)
upper case(string)
lower case(string)
substring before(string,match)
substring after(string,match)
contains(string,match)
starts with(string,match)
ends with(string,match)
matches(input,pattern)
replace(input,pattern,replacement,flags)
split(string,delimiter)
extract(string,pattern)
Context
get value(context,key)
get entries(context)
put(context,key,value)
put all(entries)
Decisions
boxed expression(context,expression)
decision table(output, input, rule list, hit policy)
(supported hit policies: "U"|"Unique","A"|"Any","F"|"First","R"|"Rule order","C"|"Collect","C+"|"C<"|"C>"|"C#")
Complete (complex) decisions
Also complex decisions like the example under assets/Sample.dmn can be written as a complex FEEL expression and evaluated - here for example as a context returning the last evaluated context entry.
const Interpreter = require("../lib/interpreter.js");
const interpreter = new Interpreter();
let exp = `
{ "Lender Acceptable DTI": function () 0.36,
"Lender Acceptable PITI": function () 0.28,
"DTI": function (d,i) d/i,
"PITI": function (pmt,tax,insurance,income) (pmt+tax+insurance)/income,
"Credit Score.FICO": Credit Score.FICO,
"Credit Score Rating": decision table(
inputs: ["Credit Score.FICO"],
outputs: ["Credit Score Rating"],
rule list: [
[>=750,"Excellent"],
[[700..750),"Good"],
[[650..700),"Fair"],
[[600..650),"Poor"],
[< 600,"Bad"]
],
hit policy: "U"
).Credit Score Rating,
"Client DTI": DTI(d: Applicant Data.Monthly.Repayments + Applicant Data.Monthly.Expenses, i: Applicant Data.Monthly.Income),
"Client PITI": PITI(
pmt: (Requested Product.Amount*((Requested Product.Rate/100)/12))/(1-(1/(1+(Requested Product.Rate/100)/12)**-Requested Product.Term)),
tax: Applicant Data.Monthly.Tax,
insurance: Applicant Data.Monthly.Insurance,
income: Applicant Data.Monthly.Income
),
"Back End Ratio": if Client DTI <= Lender Acceptable DTI()
then "Sufficient"
else "Insufficient",
"Front End Ratio": if Client PITI <= Lender Acceptable PITI()
then "Sufficient"
else "Insufficient",
"Loan PreQualification": decision table(
outputs: ["Qualification","Reason"],
inputs: ["Credit Score Rating","Back End Ratio","Front End Ratio"],
rule list: [
[["Poor","Bad"],-,-,"Not Qualified","Credit Score too low."],
[-,"Insufficient","Sufficient","Not Qualified","Debt to income ratio is too high."],
[-,"Sufficient","Insufficient","Not Qualified","Mortgage payment to income ratio is too high."],
[-,"Insufficient","Insufficient","Not Qualified","Debt to income ratio is too high AND mortgage payment to income ratio is too high."],
[["Fair","Good","Excellent"],"Sufficient","Sufficient","Qualified","The borrower has been successfully prequalified for the requested loan."]
],
hit policy: "F"
)
}.Loan PreQualification
`
let success = interpreter.parse(exp);
if (!success) console.log(interpreter.error);
result = interpreter.evaluate(exp,{
"Credit Score": { FICO: 700 },
"Applicant Data": { Monthly: { Repayments: 1000, Tax: 1000, Insurance: 100, Expenses: 500, Income: 5000 } },
"Requested Product": { Amount: 600000, Rate: 0.0375, Term: 360 }
});
console.log(result);
// {
// Qualification: 'Qualified',
// Reason: 'The borrower has been successfully prequalified for the requested loan.'
// }