imicros-feel-interpreter
Version:
DMN decisions and FEEL language interpreter
333 lines (296 loc) • 13.6 kB
Markdown
# 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 year
- `now().minute` --> current minute
- `day 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 date
- `number(from)` with from type string
- `string(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 interval
- `after(a,b)` with a,b either point or interval
- `meets(a,b)` with a,b intervals
- `met by(a,b)` with a,b intervals
- `overlaps(a,b)` with a,b intervals
- `overlaps before(a,b)` with a,b intervals
- `overlaps after(a,b)` with a,b intervals
- `finishes(a,b)` with a eiter point or interval and b interval
- `finished by(a,b)` with a interval and b either point or interval
- `includes(a,b)` with a interval and b either point or interval
- `during(a,b)` with a eiter point or interval and b interval
- `starts(a,b)` with a eiter point or interval and b interval
- `started by(a,b)` with a interval and b either point or interval
- `coinsides(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.'
// }
```