Accumulating on inner collections in Drools - drools

Let's say I have red and green baskets holding boxes of strawberries and grapes:
public class Basket {
public enum Color { RED, GREEN };
private Color color;
private List<FruitBox> fruitBoxes;
//Assume appropriate constructors, getters exist
}
public class FruitBox {
public enum Fruit { STRAWBERRY, GRAPE };
private Fruit fruit;
//Assume appropriate constructors, getters exist
}
And I need to use Drools to print the following calculations:
The number of GRAPE or STRAWBERRY FruitBoxes in a Basket if it exceeds two
The total number of FruitBoxes, grouped on FruitBox fruit and Basket Color
How might I code (1.) and (2.) as simple and efficient Drools rules? After reading the documentation, I have come up with the following solutions:
rule "Two or more of same fruit in any Basket"
when
//Bind the contents of each basket
$Basket : Basket($FruitBoxes : fruitBoxes)
//Compute A list of the unique fruits in current basket contents
$fruits : Object() from accumulate( FruitBox($f : fruit) from $FruitBoxes, collectSet($f) )
//Match and bind each unique fruit one by one
$fruit : FruitBox.Fruit() from $fruits
//Find the number of times the unique fruit occurs in this Basket
$count : Number(intValue >= 2) from accumulate(
$f : FruitBox(fruit == $fruit) from $FruitBoxes,
count($f) )
then
System.out.println($Basket + " has " + $count + " of the fruit " + $fruit);
end
Which successfully prints, for one test dataset, output like:
com.sample.DroolsTest$Basket#10cd6753 has 4 of the fruit STRAWBERRY
com.sample.DroolsTest$Basket#10cd6753 has 3 of the fruit GRAPE
com.sample.DroolsTest$Basket#2e060819 has 2 of the fruit GRAPE
My concern is that my rule is long and complicated, and appears to iterate each Basket several times (once to find the unique fruits within, and then once more to tally each fruit). I suspect that this is a product of my lack of Drools knowledge and that a better solution exists. In plain Java, I would use a HashMap to store each fruit and its count in one pass, which would be simple and fast. Is there any way to rewrite my Drools code to improve performance and readability? The same question applies to my second rule:
rule "Totals by fruit and by basket color"
when
//Calculate the cross product of all fruits and colors
$fruit : FruitBox.Fruit()
$color : Basket.Color()
//Count the number of FruitBoxes of the chosen fruit in Baskets of the chosen color
$count : Number() from accumulate(
$fb : FruitBox(fruit == $fruit)
and exists Basket(color == $color, fruitBoxes contains $fb),
count($fb) )
then
System.out.println($count + " " + $fruit + " in " + $color + " baskets");
end
Which produces output like:
5 STRAWBERRY in GREEN baskets
6 STRAWBERRY in RED baskets
6 GRAPE in GREEN baskets
4 GRAPE in RED baskets
but, unfortunately, looks like it searches through all the Baskets for every FruitBox, when it would make far more sense to tally up one (or all) kind(s) of FruitBoxes while iterating over the Baskets once. Is there a better syntax for this rule as far as performance is concerned?
Thanks for the help!

Hi for the first question I would use two separate rules:
rule "strawberriesInBasket"
dialect "mvel"
when
$basket : Basket($fruitBoxes : fruitBoxes)
Number($count : intValue() > 2) from accumulate ($fruitBox : FruitBox(fruit.name == "strawberry") from $fruitBoxes, count($fruitBox))
then
System.out.println("Count of strawberry boxes in basket is: " + $count )
end
and
rule "grapesInBasket"
dialect "mvel"
when
$basket : Basket($fruitBoxes : fruitBoxes)
Number($count : intValue() > 2) from accumulate ($fruitBox : FruitBox(fruit.name == "grape") from $fruitBoxes, count($fruitBox))
then
System.out.println("Count of grape boxes in basket is: " + $count )
end
Please notice I changed the enum Fruit to data object with field name.

Related

OptaPlanner: Drools rule on consecutive shift assignments

The context is Employee Shift Assignment with OptaPlanner using Drools rules for calculating scores.
My Employees cannot work for, say, for more than three consecutive days without a rest day.
I implement such a constraint very stupidly as:
rule "No more than three consecutive working days"
when
ShiftAssignment(
$id1 : id,
$empoloyee : empoloyee != null,
$shift1 : shift
)
ShiftAssignment(
id > $id1,
empoloyee == $empoloyee,
shift.isConsecutiveDay($shift1),
$id2 : id,
$shift2 : shift
)
ShiftAssignment(
id > $id2,
empoloyee == $empoloyee,
shift.isConsecutiveDay($shift2),
$id3 : id,
$shift3 : shift
)
ShiftAssignment(
id > $id3,
empoloyee == $empoloyee,
shift.isConsecutiveDay($shift10)
)
then
scoreHolder.penalize(kcontext);
end
I hope the name of the methods/variables clearly reveal what they do/mean.
Is there a more convenient and smart way to implement such a rule? Keep in mind that the three days above may need to change to a bigger number (I used three to avoid a more realistic ten and more lines of code in the rule). Thanks.
If we can assume an employee takes up to a single shift per day and the shift.isConsecutiveDay() may be replaced by something like shift.day == $shift1.day + 1, exists can be used:
when
ShiftAssignment($employee : empoloyee != null, $shift1 : shift)
exists ShiftAssignment(employee == $employee, shift.day == $shift1.day + 1)
exists ShiftAssignment(employee == $employee, shift.day == $shift1.day + 2)
exists ShiftAssignment(employee == $employee, shift.day == $shift1.day + 3)
then
If such an assumption cannot be made, your solution should work, with one potential corner case to think about:
The rule tries to filter out combinations of the same shifts by the condition id > $id1. This condition works, but the IDs must be generated ascendingly by the time of the shift, otherwise, it clashes with shift.isConsecutiveDay(...). In case this property cannot be guaranteed, checking for ID inequality could be preferable.
I used a combination of rules to achieve this. First rule sets up the start of a consecutive work sequence, second one sets up the end, 3rd rule creates a "Work Sequence" to fit between the start and end. Finally the "Max Consecutive Days" rule actually checks your "Work Sequence" against a limit on number of consecutive days.
This paradigm is actually in the nurse rostering example:
https://github.com/kiegroup/optaplanner/blob/master/optaplanner-examples/src/main/resources/org/optaplanner/examples/nurserostering/solver/nurseRosteringConstraints.drl

How to fix number of females employees in a shift

I am adding a custom rule for Employee shift where i have 4 types of shifts and in one type of shift the number of female employees have to be fix
i have tried adding a field in shift class namely requiredFemalesEmployees which is set to 1
//hard constraint
rule "OneFemaleInShiftA"
when
$gender:Employee(gender=="F")
$rfe:Shift(requiredFemalesEmployees==1)
accumulate(
$a:ShiftAssignment(employee==$gender,$shift:shift.requiredFemalesEmployees),
$total :count($a)
)
then
if($total.intValue()!=1){
scoreHolder.addHardConstraintMatch(kcontext, - 1);
}
end
any suggestions will be a great help.
First, you created a variable named $rfe, but is unused, and in this line:
$a:ShiftAssignment(employee==$gender,$shift:shift.requiredFemalesEmployees), what'r you assigning to $shift ?
This is my example:
rule "oneFemaleInShift"
when
$gender:Employee(gender=="F")
$rfe:Shift(requiredFemalesEmployees==1)
Number(intValue!=1) from accumulate(
$a:ShiftAssignment(employee==$gender, ¿¿$shift:shift.requiredFemalesEmployees??),
count($a)
)
then
scoreHolder.addHardConstraintMatch(kcontext, - 1);
We need the domain model or the source of java POJOs to know the relations between them.
I think this will help you:
rule "oneFemaleInShift"
when
$femaleEmployee:Employee(gender=="F") //GET FEMALE POJOS
$rfe:Shift(requiredFemalesEmployees==1) // GET SHIFT WHERE FEMALE IS REQUIRED
Number(intValue > 0) from accumulate( //COUNT NUMBER OF FEMALE EMPLOYEES IN THAT SHIFT, PENALIZE SOLUTION WHERE THERE ARE LESS THAN 1
$a:ShiftAssignment(employee==$femaleEmployee, shift==$rfe),
count($a)
)
then
scoreHolder.addHardConstraintMatch(kcontext, - 1); // LOOK AT THE VALUE OF HARD SCORE, PROPORTION WITH OTHER HARD CONSTRAINT

Breaking from inside a collect in Drools

I'm new to Drools, so I apologize if this is basic. But how do I break in the middle of a collect? For example, in the following code
c : Customer()
items : List( size == c.items.size )
from collect( Item( price > 10 ) from c.items )
This code checks if all items have a price > 10. But if I want to see if any of the items have a price > 10, what do I do? I can change code to size > 0 instead of size == c.items.size, but that would still mean the collect iterates through all the items. Is it possible to break if any of the items match the condition from within the collect?
If you just want to check for existence, then you can use the exists operator:
rule "Sample"
c : Customer()
exists Item( price > 10 ) from c.items
then
//...
end
In this case, you don't even need to use a collect. The from keyword will "loop" over all of the items in the collection.
You can check the Drools' Manual for more information about this Conditional Element.
Hope it helps,

How do accumulate functions actually work?

Let's say we have the next example :
There are certain products that belong to certain product groups, and we want the total price summed up in an logical fact as either the products in the product group change or as their price changes.
private class ProductGroup {
private String name;
}
public class Product {
private ProductGroup productGroup;
private int price;
}
This is the class that will be intended for the logical facts that will get inserted by the summation rule in Drools.
private class ProductGroupTotalPrice {
private ProductGroup productGroup;
private int totalPrice;
}
There is a rule that sums up the total price for a given ProductGroup.
rule "total price for product group"
when
$productGroup : ProductGroup()
$totalPrice : Number() from accumulate(
Product(productGroup == $productGroup, $price : price),
sum($price)
)
then
insertLogical(new ProductGroupTotalPrice($productGroup, $totalPrice));
end
So my question is what will the logic be when Products from a given ProductGroup are added/deleted from the working memory, they change the ProductGroup or their price is being changed?
- Lets say that the summation is done at the beggining of the application based on the current state and the logical fact is inserted into the working memory with the total price. Then the price for one Product is changed at one point so the totalPrice needs to be updated.
Here are three cases how the process would possibly be done :
Incrementally with doing a constant time calculation. Only take into account the change that has happened and subtract the old price from the total and add the new one for the one Product that was changed. (Excelent)
The whole summation is done again but the Product instances that meet the criteria(that are from the given ProductGroup) are already known, they are not searched for. (Good)
Besides the summation a loop through all the Product instances in the working memory is done to see which ones meet the criteria(that are from the given ProductGroup). (Bad)
Is the logic that is implemented one of these three cases or it is something else?
You can look at the documentation of the other form of accumulate, i.e., the one where you can define the steps for initialization, processings (note the plural!) and returning an arbitrary function. Some functions permit the reverse operation so that removing a fact that has been used for computing the function result can be handled: e.g., 'sum'. (But compare 'max'.)
So I think that your accumulate pattern will be updated efficiently.
However, I think that this does not mean that your logically inserted ProductGroupTotalPrice will be updated. (Try it, I may be wrong.)
I would use a simple rule
rule "total price for product group"
when
$productGroup: ProductGroup()
Number( $totalPrice: intValue ) from accumulate(
Product(productGroup == $productGroup, $price : price),
sum($price)
)
$pgtp: ProductGroupTotalPrice( productGroup == $productGroup,
totalPrice != $totalPrice )
then
modify( $pgtp ){ setTotalPrice( $totalPrice ) }
end
and an addition rule to insert an initial ProductGroupTotalPrice for the product group with totalPrice 0.

drools - get index of item in collection

I have a restaurant app and have a rule where a 20% discount needs to be offered for every 2nd ice-cream.
So,
If bill has 2 icecreams, 20% discount on the 2nd icecream
If bill has 3 icecreams, still 20% discount on the 2nd icecream
If bill has 4 icecreams, 20% discount on the 2nd and 4th icecreams
I have a collection called $bill.items which contains each individual item in the bill.
How can I write this rule in Drools given that there seems to be no way to access the index of an element in a collection.
Just collect them up and apply the discounts on the right-hand-side:
rule "Discount multiple ice creams"
when
$bill : Bill()
$iceCreams : ArrayList( size > 1 ) from $bill.items
then
for (int i = 0; i < $iceCreams.size(); i++) {
if (i % 2 == 0) {
// Apply a discount
}
}
end
Or if each bill item is available in working memory, the following can be used on the LHS to collect them:
$iceCreams : ArrayList( size > 1 )
from collect( BillItem(type == "Ice Cream") )
You may need to re-sort the list you have collected, based on each item's index within the bill.
Although, does the order of the items on a single bill really matter? The order in which items are entered on a bill is a rather unusual basis for a discount. As a customer buying 2 ice creams of differing price, I would ask for the cheapest item first because I will get a bigger discount on the second ice cream added to my bill. Hence why such discounts are usually applied to the N cheapest items. i.e. If 4 ice creams are purchased, then the 2 cheapest are discounted. Also, are ice creams different prices? If each ice cream is the same price, then all you really need to know is how many need to be discounted.