The OpenRulesTM Tutorials Home | Decision tables |
Using Natural Language Expressions Inside OpenRules Tables
This document will explain how rules could be expressed in plain English in OpenRules decision tables. The concrete examples will be shown to demonstrate the use of natural language expressions to define different rules. We will take a relatively complex decision table and will show how to use predefined OpenRules types such as FromToInt, CompareInt, and DomainString to simplify this table for a business user. While simplicity is always a matter of taste, the actual objective is to demonstrate the use of natural language inside OpenRules tables.
Comparing Integer and Real Numbers
Representing Domains of Numbers
We will use a decision table "DebtResearchRules" from the sample rule project "Loan1" included into the standard OpenRules installation. Here is the default representation of the debt research rules:
Rules void DebtResearchRules(LoanRequest loan, Customer c) | ||||||||||||||
C1 | C2 | C3 | C4 | C5 | C6 | C7 | A1 | |||||||
c.mortgageHolder.equals(YN) |
c.outsideCreditScore>min && c.outsideCreditScore<=max |
c.loanHolder.equals(YN) | op.compare(c.creditCardBalance,value) | op.compare(c.educationLoanBalance,value) | contains(rates,c.internalCreditRating) | c.internalAnalystOpinion.equals(level) |
loan.debtResearchResult = level; out("Debt Research Result:"+level); |
|||||||
String YN | int min | int max | String YN | Operator op | int value | Operator op | int value | String[] rates | String level | String level | ||||
IF Mortgage Holder |
AND Outside Credit Score |
AND Loan Holder |
AND Credit Card Balance |
AND Education Loan Balance |
AND Internal Credit Rating |
AND Internal Analyst Opinion |
THEN Debt Research Recommendations |
|||||||
Min | Max | Oper | Value | Oper | Value | |||||||||
Yes | High | |||||||||||||
No | 100 | 550 | High | |||||||||||
No | 550 | 900 | Yes | <= | 0 | Mid | ||||||||
No | 550 | 900 | Yes | > | 0 | > | 0 | High | ||||||
No | 550 | 900 | Yes | > | 0 | <= | 0 | A | B | C | High | |||
No | 550 | 900 | Yes | > | 0 | <= | 0 | D | F | Mid | ||||
No | 550 | 900 | No | > | 0 | Low | ||||||||
No | 550 | 900 | No | <= | 0 | <= | 0 | Low | ||||||
No | 550 | 900 | No | <= | 0 | > | 0 | D | F | High | ||||
No | 550 | 900 | No | <= | 0 | > | 0 | A | B | C | Low | |||
High | High | |||||||||||||
Mid | Mid | |||||||||||||
Low | Low | |||||||||||||
We are going to modify conditions C2, C4, C5, and C6 by using different OpenRules representation options.
The condition C2 above represents the fact that customers's outside credit score should be between min and max. To be exact, the condition is presented as the following Java snippet:
c.outsideCreditScore>min && c.outsideCreditScore<=max
where min and max are parameters defined in two different sub-columns of the column C2. Note that the comparison logic is hard-coded inside implementation: c.outsideCreditScore is strictly greater than min and less than or equals to max. We may easily switch to only one column and allow a business user him/herself to define the comparison logic for each particular rule using any reasonable representation of an interval min-max. Here is an example of the properly modified condition C2:
C2 |
interval.contains(c.outsideCreditScore) |
FromToInt interval |
AND Outside Credit Score |
(100;550] |
more than 550 and less or equal to 900 |
greater than 550 but less 901 |
551 - 900 |
551-900 |
551-900 |
551-900 |
from 551 to 900 |
between 551 and 900 |
Instead of two integer parameters min and max we are using one parameter interval of the predefined type FromToInt. This allows us to use different texts inside rule cells to represent actual conditions. Hopefully, this representation is intuitive enough. However, we have to remember that natural language is not exact, and we have to make some assumptions. For example, if it is commonly known that the interval (100;550] means 100 < c.outsideCreditScore <= 550. Yet, in such expressions as "100-550" or "from 100 to 550" OpenRules assumes that both bounds 100 and 550 are included: 100 <= c.outsideCreditScore <= 550.
You also may use many other ways to represent an interval of integers by specifying their two bounds or sometimes only one bound. Here are some examples of valid integer intervals:
Cell Expression | Comment |
5 |
equals to 5 |
[5,10] |
contains 5, 6, 7, 8, 9, and 10 |
5;10 |
contains 5, 6, 7, 8, 9, and 10 |
[5,10) |
contains 5 but not 10 |
5 - 10 |
contains 5 and 10 |
5-10 |
contains 5 and 10 |
5- 10 |
contains 5 and 10 |
-5 - 20 |
contains -5 and 20 |
-5 - -20 |
error: left bound is greater than the right one |
-5 - -2 |
contains -5 , -4, -3, -2 |
from 5 to 20 |
contains 5 and 20 |
less 5 |
does not contain 5 |
less than 5 |
does not contain 5 |
less or equals 5 |
contains 5 |
less or equal 5 |
contains 5 |
less or equals to 5 |
contains 5 |
smaller than 5 |
does not contain 5 |
more 10 |
does not contain 10 |
more than 10 |
does not contain 10 |
10+ |
more than 10 |
>10 |
does not contain 10 |
>=10 |
contains 10 |
between 5 and 10 |
contains 5 and 10 |
no less than 10 |
contains 10 |
no more than 5 |
contains 5 |
equals to 5 |
equals to 5 |
greater or equal than 5 and less than 10 |
contains 5 but not 10 |
more than 5 less or equal than 10 |
does not contain 5 and contains 10 |
more than 5,111,111 and less or equal than 10,222,222 |
does not contain 5,111,111 and contains 10,222,222 |
[5'000;10'000'000) |
contains 5,000 but not 10,000,000 |
[5,000;10,000,000) |
contains 5,000 but not 10,000,000 |
(5;100,000,000] |
contains 5,000 and 10,000,000 |
You may use many other ways to represent integer intervals as you usually do in plain English. The only limitation is the following: min should always go before max!
Similarly to integer intervals, one may use the predefined type FromToDouble to represent intervals of real numbers. The bounds of double intervals could be integer or real numbers such as [2.7; 3.14).
Now we will replace conditions C4 and C5 of the original decision table. The condition C4 above represents the fact that customers's credit card balance should be less or strictly less that a certain value. The basic implementation uses two sub-columns: one for a comparison operator and another one for a value, with which c.creditCardBalance should be compared. We may use only one column and allow a business user to define any comparison operator together with a value inside the same cell. Here is an example of the properly modified condition C4:
C4 |
value.compare( c.creditCardBalance) |
CompareToInt value |
AND Credit Card Balance |
<= 0 |
>0 |
> 0 |
>0 |
>0 |
<=0 |
<=0 |
<=0 |
Here we use the predefined type CompareToInt that is a simplified version of FromToInt. Comparison operators should go before a comparison value. Examples of acceptable operators:
Cell Expression | Comment |
<= 5 | less or equals to 5 |
< 5 | strictly less than 5 |
> 5 | strictly more than 5 |
>= 5 | more or equals to 5 |
!= | not equal to 5 |
5 | equals to 5. Note that absence of a comparison operator means equality. You cannot use an explicit operator "=" (not to be confused with Excel's formulas). |
Similarly to CompareToInt one may use the predefined type CompareToDouble to represent comparisons with real numbers. The comparison values may be presented as integer or real numbers, e.g. "<= 25.4" and "> 0.5".
The modified decision table with corrected conditions C2, C4, and C5 will look like:
Rules void DebtResearchRules(LoanRequest loan, Customer c) | |||||||||||
C1 | C2 | C3 | C4 | C5 | C6 | C7 | A1 | ||||
c.mortgageHolder.equals(YN) | interval.contains(c.outsideCreditScore) | c.loanHolder.equals(YN) | value.compare( c.creditCardBalance) | value.compare(c.educationLoanBalance) | contains(rates,c.internalCreditRating) | c.internalAnalystOpinion.equals(level) |
loan.debtResearchResult = level; out("Debt Research Result:"+level); |
||||
String YN | FromToInt interval | String YN | CompareToInt value | CompareToInt value | String[] rates | String level | String level | ||||
IF Mortgage Holder |
AND Outside Credit Score |
AND Loan Holder |
AND Credit Card Balance |
AND Education Loan Balance |
AND Internal Credit Rating |
AND Internal Analyst Opinion |
THEN Debt Research Recommendations |
||||
Yes | High | ||||||||||
No | (100;550] | High | |||||||||
No | more than 550 and less or equal to 900 | Yes | <= 0 | Mid | |||||||
No | greater than 550 but less 901 | Yes | >0 | >0 | High | ||||||
No | 551 - 900 | Yes | > 0 | <= 0 | A | B | C | High | |||
No | 551-900 | Yes | >0 | <=0 | D | F | Mid | ||||
No | 551-900 | No | >0 | Low | |||||||
No | 551-900 | No | <=0 | <= 0 | Low | ||||||
No | from 551 to 900 | No | <=0 | >0 | D | F | High | ||||
No | between 551 and 900 | No | <=0 | >0 | A | B | C | Low | |||
High | High | ||||||||||
Mid | Mid | ||||||||||
Low | Low | ||||||||||
901+ | ? |
Now let's look at condition C6. It states that customer's internal credit score should be one of several acceptable rates such as "A B C" and "D F". To avoid a necessity to create multiple sub-columns for similar conditions, we may put all possible string values inside the same cell and separate them by space or commas. Here is an example of the properly modified condition C6:
C6 |
domain.contains(c.internalCreditRating) |
DomainString domain |
AND Internal Credit Rating |
A B C |
D F |
D F |
A B C |
Here we use the predefined type DomainString that defines a domain of strings (words) separated by whitespaces. The method "contains(String string)" of the class DomainString checks if the parameter "string" is forund among all strings listed in the current "domain". You also may use the method "containsIgnoreCase(String string)" that allows to ignore the case during the comparison.
If possible values may contain several words, one may use the predefined type DomainStringC where "C" indicates that commas will be used as a string separator. For example, we may use DomainStringC to specify such domain as "Very Hot, Hot, Warm, Cold, Very Cold".
The modified decision table with corrected conditions C2, C4, C5, and C6 will look like:
Rules void DebtResearchRules(LoanRequest loan, Customer c) | |||||||
C1 | C2 | C3 | C4 | C5 | C6 | C7 | A1 |
c.mortgageHolder.equals(YN) | interval.contains(c.outsideCreditScore) | c.loanHolder.equals(YN) | value.compare(c.creditCardBalance) | value.compare(c.educationLoanBalance) | domain.contains(c.internalCreditRating) | c.internalAnalystOpinion.equals(level) |
loan.debtResearchResult = level; out("Debt Research Result:"+level); |
String YN | FromToInt interval | String YN | CompareToInt value | CompareToInt value | DomainString domain | String level | String level |
IF Mortgage Holder |
AND Outside Credit Score |
AND Loan Holder |
AND Credit Card Balance |
AND Education Loan Balance |
AND Internal Credit Rating |
AND Internal Analyst Opinion |
THEN Debt Research Recommendations |
Yes | High | ||||||
No | (100;550] | High | |||||
No | more than 550 and less or equal to 900 | Yes | <= 0 | Mid | |||
No | greater than 550 but less 901 | Yes | >0 | >0 | High | ||
No | 551 - 900 | Yes | > 0 | <= 0 | A B C | High | |
No | 551-900 | Yes | >0 | <=0 | D F | Mid | |
No | 551-900 | No | >0 | Low | |||
No | 551-900 | No | <=0 | <= 0 | Low | ||
No | from 551 to 900 | No | <=0 | >0 | D F | High | |
No | between 551 and 900 | No | <=0 | >0 | A B C | Low | |
High | High | ||||||
Mid | Mid | ||||||
Low | Low | ||||||
>900 | ? |
If you need to represent domains of integer or double values, there are several predefined types similar to DomainString:
For example, here is a condition column with eligible loan terms:
C8 |
domain.contains(c.loanTerm) |
DomainIntC domain |
Eligible Loan Terms |
24,36,72 |
36,72 |
72 |
Instead of using any predefined types it is possible to use Java expressions placed directly inside decision table cells. There are two ways to do it:
The following is the condition C4 represented with Java expressions:
C4 |
truefalse |
boolean truefalse |
AND Credit Card Balance |
{ c.creditCardBalance <= 0; } |
{ c.creditCardBalance >0; } |
{ c.creditCardBalance > 0; } |
{ c.creditCardBalance>0; } |
:= c.creditCardBalance >0 |
{ c.creditCardBalance <= 0; } |
{ c.creditCardBalance <=0; } |
{ c.creditCardBalance <=0; } |
Java expressions inside table cells are useful when you have many completely different conditions and want to avoid creating too many columns.
All mentioned above predefined types are implemented in the Java package com.openrules.types. You may get the source code of this package and expand and/or customize the proper classes. In particular, for internationalization purposes you may translate the English key words to utilize in your national language. You may change the default assumptions about inclusion/exclusion of bounds inside integer and real intervals. You may add new types of intervals and domains.
The use of expressions inside OpenRules tables come with some price - mainly in the performance. It is understandable because for every cell with an expression OpenRules will create a separate instance of the proper Java class during rules execution. A slowdown in rules execution maybe essential especially for large decision tables. A rules table representation provided in the basic example is probably the most efficient one. However, having multiple representation options allows a rules designer to find a reasonable compromise between performance and expressiveness.