Drools - using accumulate to find min and max - drools

I have a drools question which has been troubling me for some time. I want to find out the min and max price from a list of Item objects (contains price) using accumulate. A Member object (which contails list of Item objects) is inserted which contains the list of Items.
groovy/java source pseudo code
-------------------------------
class Item {
BigDecimal price
}
class Member {
List<Item>> items
}
...
droolsStatefulSession.insert(member)
session.fireAllRules()
...
rule.drl
---------
rule "rule1"
when
member : Member ($itemList : items)
/*
*/
then
System.out.println("condition met...")
end
Now the questions is in the above rule is it possible to if so how do I find out the item with the minimum Price and maximum price using drools accumulate feature. I do not want to use an java/groovy utility functions.
I see the "collect" feature allows to use "from" and then a datasource. I wonder if "accumulate" is similar to collect.

No need to use accumulate, just do something like
Item($lowestPrice : price, $id : id)
not Item(price > $lowestPrice, id < $id)
That's if your Items are inserted into the working memory.

I'm new to the drool rule and I'm trying to solve this issue.
You can find out min and max price of item simply - you have to write some rules and need to add two variable in the Order class:
Class Order
{
private List<Item> itemList;
private int highPrice;
private int lowPrice;
}
Using the following rule you can calculate min and max value of an item:
package com.sample
import com.sample.DroolsTest.Message;
rule "rule1"
when
$order : Order($itemList:itemList)
$item:Item() from $order.itemList
then
insertLogical($item);
end
rule "highPriceRule"
when
$order : Order()
$item:Item($price:price,price>=$order.getHighPrice())
then
$order.setHighPrice($item.getPrice());
end
rule "lowPriceRule"
when
$order : Order()
$item:Item($price:price,price<=$order.getLowPrice()||$order.getLowPrice()==0)
then
$order.setLowPrice($item.getPrice());
end
rule "highPrice"
salience -1
when
$order : Order()
then
System.out.println( "higher Item Price is "+$order.getHighPrice());
end
rule "LowPrice"
salience -1
when
$order : Order()
then
System.out.println( "Lower Item Price is "+$order.getLowPrice());
end
in main method you have to write this code and run it
List<Item> items=new ArrayList<Item>();
Item item1=new Item();
item1.setPrice(10);
Item item2=new Item();
item2.setPrice(20);
Item item3=new Item();
item3.setPrice(10);
Item item4=new Item();
item4.setPrice(5);
items.add(item1);
items.add(item2);
items.add(item3);
items.add(item4);
Order order=new Order();
order.setItemList(items);
ksession.insert(order);
ksession.fireAllRules();
output:---
Lower Item Price is 5
higher Item Price is 20
As I'm new to the drool rule I would like to ask if this is the correct procedure? Or is there another way to solve this problem?

Related

Applying Drools decision tables to 1-n data structures

I have the following classes, where there's a 1 - n relationship between Customer and Order, i.e. each Customer has many Orders
class Customer {
string identifier;
string country;
Collection orders;
}
class Order {
string identifier;
float amount;
}
class Report {
string identifier;
string country;
float amount;
}
I want to write the following in the form of one or more Drools decision tables.
For each Customer c
if c.country == US then
for each Order o
if o.amount > $10 then
create Report r
r.country = c.country
r.amount = o.amount
How possible is this using Drools decision tables?
When a Customer object meets the Customer condition, I then need to run each instance in the collection of Orders through the Order condition. If the order meets the condition, I need to create a report object that has values taken from the Customer and from the Order.
Drools will naturally iterate through collections.
This what your rule would look like in DRL:
rule "US Customer - Create Reports"
when
$customer: Customer( country == "US", $orders: orders != null )
$order: Order( amount > 10 ) from $orders
then
Report r = new Report();
r.country = $customer.country;
r.amount = $order.amount;
// TODO: do something with Report r here
end
This flattens naturally into a decision table in a very straight-forward fashion. You can even sub out the "US" (country) and 10 (amount) to variables.

Drools using from inside accumulate from

In drools I can do something like this
rule "rule1"
dialect "java"
no-loop true
when
$order: Order( $cust: customer )
Customer(id == "213123") from $cust
then
end
class Order {
private String areaCode;
private Customer customer;
}
class Customer {
private String id;
}
I want rule to identify if there are more than 3 different customers that ordered from same areaCode within an hour. Suppose a new order came in and I want to checkout if there is 3 or more orders from different customers to the same area within an hour.
rule "rule2"
dialect "java"
no-loop true
when
$order: Order( $cust: customer, $areaCode: areaCode)
Customer( $custId: id) from $cust
Set( size >= 3 ) from accumulate (
Order( $id: id, areaCode == $areaCode, customer.id != $custId ) over window:time( 1h ),
collectSet( $id ) )
then
end
Can I access customer.id the way that I use in rule 1 within from accumulate?
I'm a little unsure about what exactly you're trying to do in your example "rule3", but in general yes you can have a "from" clause inside of an accumulate.
Here's an example. Assume these models (getters and setters are implied but omitted for brevity):
class Student {
private String name;
private List<Course> courses;
}
class Course {
private Double currentGrade; // [0.0, 100.0]
}
Let's say we want to write a rule where we identify students who have 3 or more classes with a grade < 70.0.
rule "Students with three or more classes with less than a 70"
when
$student: Student($courses: courses != null)
$failingCourses: List( size >= 3 ) from accumulate (
$course: Course( currentGrade < 70.0 ) from $courses,
collectList( $course )
)
then
System.out.println("Student '" + $student.getName() + "' is failing " + $failingCourses.size() + " courses.");
end
In your accumulate you can use a 'from' clause to indicate the source of the objects you're accumulating. In my case it's a list, but you can use a window or temporal operations as well.

Drools compare if one list contains element from another list

I have got below structure in Java:
public class Request {
List<Product> product;
List<Account> accounts;
}
public class Product {
String productIdOne;
String productIdTwo;
String productTax;
}
public class Account {
List<ProductRelationship> productsRelationship;
}
public class ProductRelationship {
String productIdOne;
String productIdTwo;
}
And the request is the fact object send to drools. I am wondering how I can check if there is at least one product that productTax is set to 'true' and there is a relationship between one account and one product. In other words, if there is a product with tax set to true and at least one account contains a relationship with this product (by productIdOne and productIdTwo) then the rule result should pass;
The main issue is that the list of the product relationship is inside the account list.
Thanks for any advice
You have a rather straight-forward set of conditions, so it is possible to write a relatively simple rule to check them. I will consider each condition separately and then combine them into a final rule.
As you wrote:
there is at least one product that productTax is set to 'true'
Now, as you mentioned, your rule inputs are the Request instance which contains two lists (products, accounts.) We'll start by declaring that:
rule "Account exists with taxed product"
when
Request( $products: product != null,
$accounts: accounts != null )
Next, we want to find the taxed product. If we only wanted to prove the existence of the product, we could use an exists( ... ) condition, which is extremely fast. However since we want to do further comparisons, we'll want to actuall find the product with this condition and save a reference to it.
$taxedProduct: Product( productTax == "true" ) from $products
I've assumed here that any value other than exactly "true" is indicative of an untaxed product. You should adjust as needed (and possibly consider changing this type to a boolean.)
The next condition is to find the account:
there is a relationship between one account and [the taxed] product
First, we'll need to update our $taxedProduct declaration and get references to its ids:
$taxedProduct: Product( productTax == "true",
$id1: productIdOne,
$id2: productIdTwo ) from $products
Now we need to find an account with a matching relationship.
$account: Account( $relationships: productsRelationship != null ) from $accounts
exists( ProductRelationship( productIdOne == $id1,
productIdTwo == $id2 ) from $relationships )
Here, I used an exists condition for the relationship because we don't need to refer to the relationship itself ever again, just verify that the relationship exists. I did declare a variable $account to point to the account that has the product relationship.
Putting it all together, we have:
rule "Account exists with taxed product"
when
Request( $products: product != null,
$accounts: accounts != null )
$taxedProduct: Product( productTax == "true",
$id1: productIdOne,
$id2: productIdTwo ) from $products
$account: Account( $relationships: productsRelationship != null ) from $accounts
exists( ProductRelationship( productIdOne == $id1,
productIdTwo == $id2 ) from $relationships )
then
// We have a taxed product $taxedProduct
// and an associated account $account
end
When I first started with Drools I found it difficult to wrap my head around the way it treated objects in lists, which is why the ProductRelationship sub-list seems like a tricky issue on its face. What Drools is going to do is iterate through the $products list and find those Product instances that meet our criteria (namely, have productTax == "true".) Once it has found these taxed products, it then similarly goes through the $accounts list and finds all Accounts that meet the criteria (which have a productsRelationship list.) Then for each of those accounts, it is going to test that there exists a relationship as we've defined.
This is a simplified explanation, of course, but it helps to form a mental model of roughly what Drools is doing here. In reality Drools is much more efficient than then roughly O(n^3) workflow I've just described.
An interesting thing you should keep in mind is that this rule is not going to "stop" as soon as it finds a match. If you have two taxed products that have a relationship to a single account, this rule will fire twice -- once for each taxed product. Or, alternatively, if you have one taxed product and two accounts that have a relationship to it, the rule will fire twice (once for each account.) Basically, the rule will fire once for each "match" it finds in the given request.
can you try this following
rule "sample"
no-loop
when
request:Request(accountList: accounts)
request1:Request(productList: product)
Account(productsRelationshipList:ProductRelationship) from accountList
Product(productId contains productsRelationshipList, productTax = true ) from productList
then
System.out.println("Rule fired satisfied");
end

How to dynamically count totals of records with a certain value?

I'm looking to create a count of my 'trolleys' field which is dynamic. This should count the number of trolleys on any given day by the type (e.g. a,b,c). However, I don't want to create a static column which only counts by the type (A,B,C). However, instead I would require a dynamic count which would count depending on the 'type' as we currently don't know which types will be used as this would change on a day to day basis.
Sample data:
I'm looking to create this in Ireport 5.6.
Proposed Outcome
Any ideas would be excellent :)
You can use HashMap to count your fields, like this.
public Map<String, List<Class>> sortByKey(List<Class> values){
Map<String, List<Class>> map = new HashMap<>();
for(Class value : values){
if(map.containsKey(value.type)){
List<Class> valueByKey = map.get(value.type);
valueByKey.add(value);
}
else{
List<Class> newValues = new ArrayList<>();
newValues.add(value);
map.put(value.type, newValues);
}
}
return map;
}
Code above sort your data by key, which is in your example "type" field. You can then get number of each type by checking the size of the list by the specific key. Example bellow.
List<Class> tmp = map.get("a");
int count = tmp.size();

JPA: Selecting entities based on multiple criterions on multiple child entities

I have a problem getting the following scenario to work. A student can take tests. A student have over time taken a few tests and got a score for each test. Each student entity have a list of tests that they have completed mapped as #OneToMany.
Now I want to select all students that have completed tests on a range of grouped criterions. I want for example to search for all students that have:
Group 1: Completed "Test 1" and got a score "between 75 and 100"
and/or
Group 2: Completed "Test 2" and got a score "between 50 and 80"
This is what I have so far but it does not do what I need (cannot search by multiple parameters meaning that I have to perform the query multiple times):
SELECT s FROM Student s JOIN s.tests t WHERE t.score BETWEEN :minScore AND :maxScore AND t.testName = :testName
Is there a way to use a single NamedQuery to achieve what I want? To retrieve all Students that have completed a test that matches at least one of the parameter groups above? I've been experimenting with joins but keep running into the wall.
I made a sample code skeleton below to illustrate what I'm trying to do.
#Entity
#NamedQueries({
#NamedQuery(name="Student.findStudentByParams", query="????????") // What should this query look like to satisfy the criteria? (see below for more detail)
})
public class Student {
// .. Some other variables that are not relevant for this example
#Id
private String name;
#OneToMany(fetch=FetchType.EAGER, mappedBy = "student")
private List<Test> tests;
// Setters and getters
}
#Entity
public class Test {
private double score;
private String testName;
// .. Some other variables that are not relevant for this example
#ManyToOne(cascade=CascadeType.ALL)
private Student student;
// Setters and getters
}
public class SearchParameters {
private double minScore;
private double maxScore;
private String testName;
public SearchParameters(String minScore, String maxScore, String testName) {
this.minScore = minScore;
this.maxScore = maxScore;
this.testName = testName;
}
// Setters and getters
}
public class MainClass {
public static List<Student> getStudents(List<SearchParameters> searchParams) {
// Database initialization stuff
// What should the query look like to find all students that match any of the combined requirements in the searchParams list?
// Is it possible to do in a single query or should i make multiple ones?
// What parameters should i set? Is it possible to put in the entire array and do some sort of join?
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
Query namedQuery = em.createNamedQuery("Student.findStudentByParams");
namedQuery.setParameter(??);
return (List<Student>)namedQuery.getResultList();
}
public static void main() {
List<SearchParams> searchParams = new ArrayList<SearchParams();
searchParams.add(new SearchParameters(75,100, "Test 1"));
searchParams.add(new SearchParameters(50,80, "Test 2"));
// Retrieve all students which matches any of these search parameters:
// Have either:
// Completed "Test 1" and got a score between 75 and 100
// and/or:
// Completed "Test 2" and got a score between 50 and 80
ArrayList<Student> students = getStudents(searchParams);
for(Student s: students) // Print all user that match the criteria
{
System.out.println("Name: " + s.getName());
}
}
}
You need to use Criteria Builder (and eventually the canonical Metamodel).
Try something like this (code not tested):
EntityManager em; // put here your EntityManager instance
CriteriaBuilder cb = em.getCriteriaBuilder();
CriteriaQuery<Student> cq = cb.createQuery(Student.class);
Root<Student> student = cq.from(Student.class);
Predicate predicate = cb.disjunction();
for (SearchParams param : searchParams) {
ListJoin<Student, Test> tests = student.join(Student_.tests);
Predicate tempPredicate1 = cb.equal(tests.get(Test_.testName), param.getTestName());
Predicate tempPredicate2 = cb.ge(tests.get(Test_.score), param.getMinScore());
Predicate tempPredicate3 = cb.le(tests.get(Test_.score), param.getMaxScore());
Predicate tempPredicate = cb.and(tempPredicate1, tempPredicate2, tempPredicate3);
predicate = cb.or(predicate, tempPredicate);
}
cq.where(predicate);
TypedQuery<Student> tq = em.createQuery(cq);
return tq.getResultList();
I don't see how it would be possible without composing the query dynamically. Consider using the Criteria API to create it.
I would design the query like this:
select s from Student s where
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (select t.id from Test t where t.student.id = s.id and ...)
or
exists (...)
As you see, there's a repeating pattern, and all these subqueries are similar an are combined into a disjunction.