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,
Related
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
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.
When we need to delete some items inside a queue, we may easily write code like below:
foreach(queue[i]) begin
if(queue[i].value == 1)
queue.delete(i);
end
But there is bugs in above code when queue[0]==queue[1]==1. Because queue.delete(0) will change all indexes of items inside queue.
So currently I use code as below:
foreach(queue[i]) begin
if(queue[i].value == 1) begin
queue.delete(i);
i--;
end
end
It works, but it looks confusing at first glance.
So my question is:
Are there any better solution for this issue in system verilog?
I believe this should work (I'm unable to test it right now. Make sure order is persevered when you try it out)
queue = queue.find() with ( item.value != 1 );
Another approach would be to find all the indexes that meet your criteria, sort in depending order, then loop through the indexes
int qi[$] = queue.find_index() with ( item.value == 1 );
qi = qi.sort() with ( -item ); // sort highest to lowest
foreach(qi[idx]) queue.delete(qi[idx]);
Refer to IEEE1800-2012 ยง 7.12 Array manipulation methods for details
I have the following class structure (Class A contains Class B):
class A {
B object;
...
}
and I'm trying to do something whenever the average of the accumulation of a specific field in class B is above a given value.
so I'm trying to write the following :
when
A($var1 : object)
accumulate( B($num:num) from $var1;
$avg1 : avg ($num); $avg1 < 10000)
then ...
what happens is that instead of accumulating all entities in the session and calculating the average for all of them, the average is being calculated on each entity separately.
so if the session already contains 5 numeric values which bigger than 10000 and another one is inserted then the "then" part is invoked 6 times (each one with average value the equals to the numeric value itself) instead of only once.
Do you have some hint that might help me to solve that?
thanks.
You have to accumulate over all A facts accessing the field num of field object.
when
accumulate( A( $var1: object );
$avg1: avg($var1.getNum()); $avg1 < 10000)
then ...
Inserting all B objects as facts as well would permit you to write the straightforward
when
accumulate( B( $num: num );
$avg1: avg($num); $avg1 < 10000)
then ...
I am trying to implement a rule in Drools that calculates the sum of a some property of a fact. That works great using accumulate and sum. The problem is that when there are not fact that matches the criteria in the source part of the accumulate method the rule is not executed.
I would like the sum method to return zero if no fact is matching and that the rest of the when clauses is checked. Is that possible somehow?
Update:
I am using Drools 6.0.1
The problem seems to lie in the the and clause. Here is a code that is my problem.
rule "accu"
when
$n: Number()
from accumulate( $o: Order() and OrderLine( $v: quantity ),
sum($v))
then
System.out.println("*#*#*#*#*#*#*#*#*#* Accu has fired *#*#*#*#*#*#*#*#*#");
end
With only Order or OrderLine it works. I have a feeling I am attacking the problem the wrong way. In my real case the value I want to sum up is in the OrderLine but the criteria is in another class.
$ol : OrderLine($q : quantity)
and
$ac : ArticleClass(orderLine == $ol, crtiteria1=efg, criteria2=abc)
But accumulate does return 0 when there are no matching elements.
rule accu
when
$n: Number()
from accumulate( Fact( prop == "C", $v: value ),
sum($v))
then
//...
end
This fires in the absence of Fact facts with prop == "C" and it fires if there are no Fact facts at all. (Drools 5.5.0)
Please provide full code reproducing the error, Drools version, etc.