How to handle lots of rules with shared conditions in Drools - drools

Let's supposed I'm working on a sales tax calculation application that has lots of rules for each of the states; e.g., I might have
rule "Florida Food Sales Tax"
when
$item : OrderItem( state == States.FL, category == Categories.FOOD )
then
modify($item) { setSalesTax(new BigDecimal("5") }
end
rule "Florida Non-Food Sales Tax"
when
$item : OrderItem( state == States.FL, category == Categories.NONFOOD )
then
modify($item) { setSalesTax(new BigDecimal("8") }
end
In this application I might have dozens, even hundreds of rules about Florida sales tax...and for every one of the other states as well. It gets tedious having to put the state condition in every rule. Is there a way to create some kind of nested context around rules so I only have to do that once for a whole block? Something like (and I know this isn't valid, consider this pseudo-code):
when $item : OrderItem( state == States.FL )
rule "Florida Food Sales Tax"
when
$item : OrderItem( category == Categories.FOOD )
then
modify($item) { setSalesTax(new BigDecimal("5") }
end
.
.
.
end
I know rules can extend other rules, but as far as I can tell they can only have one parent and I might want to split things up into multiple blocks; e.g., have sub-blocks for the food vs. non-food rules.

Extending rules can be continued over more than one level so that you could have a parent rule
rule "Florida Sales Tax"
when
$florida: OrderItem( state == States.FL )
then end
and at the next level two rules for food and non-food with a couple of rules like this one:
rule "Florida Food Sales Tax" extends "Florida Sales Tax"
when
$ffi: OrderItem( this == $florida, category == Categories.FOOD )
then end
Equally well, you could start the hierarchy by selecting the category first, but there's no way to inherit from more than one rule. (Besides, I doubt that the gain would be worth doing it.)
Note that you'll run into the same dilemma when using a decision table. Joining cells with the same value for a property is only possible if the resulting rules are placed next to each other - and you can't do this for all rules for more than one property at the same time.

Related

Drools, accumulate sum and then max

My java class Task has member groupId(String) and duration(long). The sum of task durations of each group, say group1 has task1, task2, task3, corresponding durations are d1, d2, d3, so the sum s1=d1+d2+d3; I want to get the largest s of all groups, say for group1 the sum is s1, group2 is s2, I want to get the $smax = max(s1,s2,...) and then do something on $smax in "then". How can I do this? There is no group class, a String named groupId is used to identify the same group. How to specify the "same-group-id"? Do I have to introduce group class? And how to get the $smax? Many thanks.
when
accumulate(Task("same-group-id", $d: duration);
$s: sum($d))
accumulate($smax: max($s))
then
// do something with $smax
Further question. Roddy's answer works perfectly in drools. But in my case it's used in Optaplanner, so Task must show up in "when" otherwise the rule will not be triggered. Is there any way to merge Roddy's two rules into one? The goal is still to get the largest group duration in "then" so that something can be done on it. I can make changes in java if it's needed.
You've got a bit of a wall of text going on here, but I'll try to parse out what you're trying to do.
You have Task objects in working memory with String groupId and long duration members
You want to group Tasks by the groupId and then sum up the durations for each group. --> basically get the total duration for each group.
Finally you want to find the longest of those summed durations. --> Effectively, you want to know which group has the longest duration.
If I'm avoiding using inline custom code in my accumulate, I'd probably do something like this:
declare GroupDuration {
id : String
duration : long
}
rule "Find Group Durations"
salience 1
when
Task( $id: groupId )
not( GroupDuration( id == $id ))
accumulate( Task( groupId == $id, $d: duration);
$duration: sum($d))
then
GroupDuration d = new GroupDuration();
d.setId($id);
d.setDuration($duration);
insert(d)
end
rule "Find Longest Group Duration"
when
GroupDuration( $id: id, $duration: duration )
not( GroupDuration( duration > $duration ))
then
// $id is the longest
end
I start off by declaring a new type GroupDuration to track the id and total duration of the group. Then I populate working memory with those durations in the first rule, using accumulate to sum up all durations for tasks that have matching IDs. Finally, I find the instance with the largest duration and yank its id.

Drools: How to get return values from Condition in Decision tables?

I have a Decision table in Drools and I'm trying to retrieve the return values of the Conditions (columns) that evaluates to see if a particular Rule (rows) is executed. May I know how if it's possible? Please find below for a simple example of my problem.
Condition 1 | Condition 2 | Condition 3 | Condition 4
Age < 60 Employed=Yes Owns a house=Yes Single=Yes
Rule 1: YES YES
Rule 2: YES NO YES
Rule 3: NO YES
Let's say if Rule 2 should be ideally executed and yet Rule 1 is executed, I would like to know the reason why Rule 1 was executed by obtaining the return values of the Condition 1 and 3 (whether it is true or false). Is there a way to do so?
The DRL rule for your Rule 1 would be something like
rule "Rule 1"
when
Person( $age: age < 60, $owner: owner == true )
then
...( $age, $owner )...
end
and on the right hand side you have binding variables $age and $owner set to the actual values as contained in the Person fact. You can use Java code in an ACTION column in a decision table to do whatever you want with those values.
Edit If you need the values for the negative case, you'll have to use another rule
rule "Rule 2"
when
Person( $age: age, $owner: owner,
$age >= 60 || $owner == false )
then
another_action...( $age, $owner )...
end
Most likely, you'll need to do some other action anyway.
Of course, individual rules for all four cases are also possible. Note that decision tables let you write rules for all four cases TT, TF, FT, FF. You can combine the action cells so you can define the action for TF FT FF in a single cell.

Sorting a list of objects using Drools rules engine

I am trying to sort a list of objects using a set of rules defined in drools rules engine.
The sample object structure is as follows
public class A {
String name;
Date createdDate;
}
I want to
Define a set of rules for sorting a list of objects .
Ex : rule 1 : "Sort objects using name ascending"
       rule 2 : "Sort objects using createdDate descending"
Define the order which the rules needs to be executed .
Ex : set order 1 to rule 1
       set order 2 to rule 2
So the objects will be sorted by name ascending and createdDate descending.
Can I achieve this using the drools engine ?
I thought of using the compareTo() for the sorting but since the sorting criteria can be changed
at runtime the logic is becoming complex and hard to maintain.
Thanks,
Kolitha.
Drools does not sort objects the way quicksort or some similar sorting algorithm rearranges objects within an array or some other aggregate. What you can do is to have a rule fire, repeatedly, once for each fact from a set of facts (such as your class A objects) and with constraints guaranteeing this happening in a certain order. Also, you need to keep track of
facts that have already been processed.
Therefore, the question to be answered first is: why do you need the objects in a certain order?
If the facts need to be processed in this order, you don't have to sort them in the ususual sense of the word, and the aforementioned rule would be sufficient:
declare ListOfA
listOfA: List
end
rule noListOfA
when
not ListOfA()
then
ListOfA loa = new ListOfA();
loa.setListOfA( new ArrayList() );
insert( loa );
end
rule sortA
when
$a: A( $name: name, $createdDate: createdDate )
$loa: ListOfA( $listOfA: listOfA not contains $a )
not A( this != $a, this not memberOf $listOfA,
name < $name ||
name == $name && createdDate > $createdDate )
then
System.out.println( $a );
modify( $loa ){ getListOfA().add( $a ) }
end
This is the way to sort in drools.
rule "Rule 03"
when
$number : Number( )
not Number( intValue < $number.intValue )
then
System.out.println("Number found with value: " + $number.intValue() );
retract( $number );
end

Drools rule for counting number of times an service was performed?

I am new to Drools and am trying to design some rules for a dental insurance application. The system will basically let users known when a procedure they are about to perform may not be covered by insurance based on the history of previously performed services. It will have other rules as well, which may be age based, but I can handle those.
My facts are:
Patient - Listing of patient information.
Services - Previously performed services. (ie: serviceCode=D1234, datePerformed=Date)
Alerts - (alertName = "Xrays 2/12 Month Period")
I need a rule which says WHEN a patient has had D1234 performed 2 or more times in the last 12 month period THEN add a warning saying that D1234 may not be covered by insurance until 12 months after date of last D1234 service.
Further complicating the situation is the fact that there could be groups of codes which are limited in the same way. So, the codes listed in the rule may be an array of codes and not just a single one, but the rule would still need to fire.
I could write a service to fetch all the services performed and just do it like that, but I would think it is nicer to just throw all the facts (previous services, etc) into Drools and let it work it all out. This way I could have a rule process run for each patient with their alerts and previous services as facts and the result with be a list of warnings.
Can someone help me understand how to write a rule like I need above?
I will show you some examples of the different things you need to do, and leave it to you to combine them into a rule which works within your application.
Warning - I have not executed the following examples, so there may be bugs/typos.
First, the following piece of code will collect up all services performed on each patient in the working memory.
rule "Count patient services"
when
$patient : Patient()
$serviceList : ArrayList() from collect (
Service(patientId == $patient.id)
)
then
System.out.println("Patient " + $patient.id
+ " has received " + $serviceList.size() + " services.");
end
The following matches when a patient has received more than 2 services with a particular code:
$serviceList : ArrayList( size > 2 ) from collect (
Service(
patientId == $patient.id,
serviceCode == "D1234"
)
)
The following matches when a patient has received more than 2 services matching a list of codes.
$serviceList : ArrayList( size > 2 ) from collect (
Service(
patientId == $patient.id,
serviceCode in ("D1234", "E5678")
)
)
The following finds the most recent matching service date:
accumulate (
Service(
patientId == $patient.id,
serviceCode in ("D1234", "E5678"),
$datePerformed: datePerformed
);
$mostRecentDate: max($datePerformed)
)
Likewise, you can add constraints on dates or other attributes.
An effective mechanism for maintaining code groups would be to insert group membership facts. A spreadsheet or web decision table can do this easily, or you could query a database and insert them via the API. i.e.
insert( new ServiceGroup( "HighCostService", "D1234" ) );
insert( new ServiceGroup( "HighCostService", "D5678" ) );
Then you can do the matching with constraints like this:
$highCostServices : ArrayList() from accumulate (
ServiceGroup( group == "HighCostService", $serviceCode ),
init( ArrayList list = new ArrayList(); ),
action( list.add($serviceCode); ),
reverse( list.remove($serviceCode); ),
result(list)
)
$serviceList : ArrayList( size > 2 ) from collect (
Service(
patientId == $patient.id,
serviceCode in $highCostServices
)
)
n.b. - Accumulators are rather easy to get wrong, so I usually put a few unit tests around them. The above code was written freehand in here without running it anywhere, so you may well be lucky if it works without corrections.
For more details, see the manual:
http://docs.jboss.org/drools/release/5.5.0.Final/drools-expert-docs/html_single/
Matching items which are in a list:
4.8.3.3.10.10. The operators in and not in (compound value restriction)
Collecting lists of matched facts:
4.8.3.7.3. Conditional Element collect
Accumulators for calculating max, min, sum, etc values for collections:
4.8.3.7.4.1. Accumulate CE (preferred syntax)

MDX: Calculated Dimensions?

I don't know if this is possible or not, or if my limited knowledge of MDX is pushing me in the wrong direction...
The cube I'm dealing with has two different dimensions for dates, [Statement Dates] and [Premium Dates]. Further in the hierarchy of each looks like this:
[Statement Dates].[Statement Year].[2008]
[Payment Dates].[Payment Year].[2008]
For the business logic I'm implementing I need to do something like:
select
({ [Measures].[Commission] }) on columns,
({[Products].[Product Category]}) on rows
from [Cube]
where
(
IIF( [Products].[Product Category].CurrentMember.Name = "Some Category",
[Statement Dates].[Statement Year].[2008],
[Payment Dates].[Payment Year].[2008] )
)
So I need it to discriminate what dimension to use for filtering the year based on what product category is being used.
This parses ok and the query runs, but the numbers seem to suggest that the IIF statement is always returning false.
Because the WHERE clause gets evaluated first the .CurrentMember function in the IIF will only be seeing "All Product Cateogories". In which case the [Products].[Product Category].CurrentMember.Name will never be equal to "Some Category" as the only product category in context is "All Product Cateogories".
One possible work around would be to do a calculation like the following:
WITH MEMBER Measures.Commission2 as
SUM(
{[Products].[Product Category].[Product Category].Members}
, IIF( [Products].[Product Category].CurrentMember.Name = "Some Category"
, ([Statement Dates].[Statement Year].[2008],[Measures].[Commission])
, ([Payment Dates].[Payment Year].[2008].[Measures].[Commission]) )
)
select
({ [Measures].[Commission2] }) on columns
, ({[Products].[Product Category]}) on rows
from [Cube]
You could also do a scoped assignement in the calculation script in your cube to do this sort of thing.