How to Group elements inside the list using drools - java

I am new to Drools and i am struggling to find the solution for below problem:
I have a list of Account class:
class Account {
private String name;
private float amount;
}
I want to group the list of Account using name in drools.
For example:
Account a = new Account("Science", 100);
Account b = new Account("Science", 200);
List<Account> list = new ArrayList<String>();
list.add(a);
list.add(b);
Now I need a drool rule that should group the elements inside the list using name and provide the list that will have "Science, 300".
Please suggest.

As Ironluca points out in the comments, you are using the wrong tool for the job. The correct solution would be to use Java streams; even a simple for-loop would solve your problem.
Of course, since you can use a hammer on a screw, you can do this with Drools. I will include an example rule and an explanation, though it will be much less efficient than a stream-based solution (or even a for-loop.)
I will assume this model, with appropriate getters, setters, and an all-args constructor:
class Account {
String name;
int quantity;
}
Then this rule will populate an 'output' list with the "grouped" accounts. I've declared this 'output' list as a global, which isn't best-practice but none of this is recommended anyway since it's not the appropriate tool for the job.
global List $output;
rule "Consolidate duplicates"
when
$inputs: List()
Account( $name: name ) from $inputs
$duplicates: List()
from collect( Account( name == $name ) from $inputs )
$total: Number()
from accumulate( Account( $value: quantity ) from $duplicates, sum( $value ) )
then
$output.add(new Account($name, $total));
end
The first part of the rule identifies the subset of Account instances in the input list which share the same name. I use collect because this is a simple subset of the original input. The next step is to use accumulate to sum up the quantities from those duplicates. Then we add a new instances to the output list on the rule's RHS with the consolidated quantity and the shared name. Note that the rule works the same for Accounts which have unique names; there is no constraint on length for $duplicates, and the accumulation would just be the one identified quantity.
Modifying $inputs in-place is also do-able, but tricky, since you have to worry about concurrent modification exceptions. The much cleaner implementation is to use
Of course this is way more complicated than it has to be. Java 8 streams provide a groupingBy collector which allows you to trivially create a map by a property within the object:
Map<String, List<Account>> accountsByName = accounts.stream()
.collect(groupingBy(Account::getName));
You could then easily transform this by iterating over the valueset.
This other question goes into detail for other 'summation of duplicates in a stream' scenarios: Java 8 stream sum entries for duplicate keys

Related

How to get cumulative amount total from list of Map

In one of my case I get the input like below which has a list inside with list of Maps.
List<Map<String, String>> actAllSavAccDetLists = test1Page.getAllSavingsAccountsDetails();
// returns like this
[
{Savings=Account ****2623, Current Balance=$22000.00, Annual Rate=7.77%, Transfer=Make a Transfer, Ellipses=...},
{Savings=Account ****5678, Current Balance=$11000.00, Annual Rate=2.22%, Transfer=Make a Transfer, Ellipses=...}
]
Now I need to find the total balance for the user, i.e.; adding up all the current balance from the Map inside a list.
Say in this case, adding $22000.00 + $11000.00 to give the result as $33000.00 in a total_bal variable.
You can easily use java stream map->reduce to make that:
Double totalBalance = actAllSavAccDetLists.stream()
.map(e -> e.get("Current Balance").substring(1))
.map(e -> new BigDecimal(e))
.reduce(BigDecimal.ZERO, BigDecimal::add);
The java-compatible way to do it is not to have this map at all. Java is nominally typed, and really likes its types. a Map<String, String> is not an appropriate data type here.
First, make a class that represents a savings account.
Next, instead of having a List<Map<String, String>>, have a List<SavingsAccount>.
Finally, sum up the balances.
Making a class
Looks like it would be something along the lines of:
#lombok.Value
public class SavingsAccount {
String accountId;
int balance; // in cents
double rate; // might need to be BigDecimal
}
You'll need to festoon it up to become a proper java class (the fields need to be final and private, getters and setters nee dto be there, a constructor, equals, toString, etcetera). I'm using lombok here (disclaimer: I'm one of the developers), but you can also use a java16 record, or use your IDE to generate all this stuff.
Converting that mess into instances of SavingsAccount
Converting a map that contains for example a mapping Transfer = Make a Transfer into an instance of this rather strongly suggests your input is coming from some bizarre source. You'll know better than we do how to convert it. You can now localize all the various required conversions and open questions into a single place. For example, what should happen if, say, map.get("CurrentBalance") doesn't exist, or returns "€10000.00"?
This boils down to "How do I convert the string "$22000.00" into the integer 2200000", or "How do I convert "7.77%" into a double", which is not difficult, and an unrelated question; if you're having trouble with it, I'm sure it's been answered a million times on SO already so you'll find it swiftly with a web search.
Summing it up
That's trivial:
List<SavingsAccount> accounts = ...;
int sum = accounts.stream().mapToInt(SavingsAccount::getBalance).sum();
This streams all the accounts, extracts just the balance from each, and then sums the entire stream into a single number.
I don't want to make that class
Well, it's a bit silly to do things in ways no sane java programmer would ever do. If you're trying to learn, you'll be learning the wrong ways of work. If you're trying to deliver freelance work, you'll get negative reviews. If you're "in a hurry", taking shortcuts now will just cost you triple later on. You do want to make that class.
If you insist on being stubborn, the same techniques can be used, just, with the order all jumbled up. You can stick the code that extracts the balance in that mapToInt call:
.mapToInt(s -> extractBalanceFromThisBizarroMap(s))
and then just write static int extractBalanceFromThisBizarroMap(Map<String, String> s) yourself.
But don't do that.

Merge a field in List to new List in Java using Stream

I have a list of this sorts
List<Employee>emp = Arrays.asList(new Employee("Jack", 29), new Employee("Tom", 24));
class Employee {
private String name;
private Integer id;
}
I want to insert to Employee full name List as follows:
List<Employee>empFullName = Arrays.asList(new Employee("Jack Tom", 29));
class EmployeeFullName {
private String fullName;
private Integer id;
}
How can I merge the name fields in Employee to fullName in Employee List after combining the names? I want to use Java 8 for the solution.
Notwithstanding all the reasonable questions previous commenters have posted, it feels to me like your main problem boils down to "How do I get pairs of objects out of a stream".
Once you have paired up the objects into a new collection (or stream) of pairs, you can do whatever you want to with them (i.e. make a new object out of them).
Collect successive pairs from a stream
You would still have to decide how to "merge" the pairs. In your case, it looks like you're taking the "name" and joining them together for each Pair to produce a fullName. And you're using the left-hand-side ID. That still leaves one to wonder what happened to the right-hand-side ID, but maybe with your real data-set, it's functionally duplicated..? Even so, it might be worth doing a programmatic Assert to make sure Pairs you're streaming out are consistent in that way. Otherwise one missing element in your stream and you'll be tying together all sorts of random users...

Create a collection by associating two id's of two different object

I have two list of objects.
members: List,
membersHistory: List
Both of these objects have an "id" field. I want to create a list by joining both of these lists by making sure the "id" field of one is associated to the other object that have the same id. Both, object have different data but they are for specific member. Just need to pair them somehow to create a collection.
I started out something like below. But, I think I need to map them by "id" first before zipping them. Thank you!
members.zip(membersHistory).mapIndexed {_, pair ->
val (member, memberHistory) = pair
}
Here are a couple of ways to create a list of pairs of the items.
For each item in the first list, find a corresponding item in the second list with the same idea and pair them if found. mapNotNull will cause it to skip items that have no match in the second list.
val combination: List<Pair<Member, MemberHistory>> = members
.mapNotNull { member -> memberHistories.firstOrNull { it.id == member.id }?.let { member to it } }
To do this in O(n), you can create a map with the IDs as keys from one of the sources lists using associateBy.
val memberHistoryById = memberHistories.associateBy { it.id }
val combination = members.mapNotNull { member -> memberHistoryById[member.id]?.let { member to it } }
Presumably, though it hasn't been stated explicitly in the question, the id is the ID of a Member, so there will be no two objects in the members list with the same id value.
For quick lookup of Member by ID, I'd recommend creating 2 maps:
// Examples in Java
Map<Integer, Member> memberById = members.stream()
.collect(Collectors.toMap(Member::getId, Function.identity()));
Map<Integer, List<History>> memberHistoryById = membersHistory.stream()
.collect(Collectors.groupingBy(History::getId));
Those are both good to keep around, but if you want the Member and the History together, you can then create a combined map, keyed by the Member object. Assuming the natural order of Member is not the ID, we need a custom key, which we can do with TreeMap.
Map<Member, List<History>> historyByMember = memberHistoryById.entrySet().stream()
.collect(Collectors.toMap(e -> memberById.get(e.getKey()),
Map.Entry::getValue,
(a,b) -> a/*this is never called*/,
() -> new TreeMap(Comparator.comparingInt(Member::getId)));
The Answer by Andreas is a good one, using a map to associate a member object with its matching history object.
Write a class
Alternatively, you could create a class to bind the two objects together.
In Java 16 and later, a record might do. A record is a brief way to write a class whose main purpose is to communicate data transparently and immutably. The compiler implicitly creates the constructor, getters, equals & hashCode, and toString. A record can be declared locally or separately.
record MemberWithHistory ( Member member , History history ) {}
Loop your list of members.
For each member, find a matching history. We can do this easily with streams. Make a stream of your history objects, filtering for one whose member identifier matches the identifier of the nth member. An Optional is returned carrying the history object if found. Otherwise, if no matching history is found, the optional carries nothing.
If the optional does indeed have a found history object, instantiate a new MemberWithHistory record object. Collect that new record by adding to our results list named mwhs.
Here is some untested code to get your started.
List< MemberWithHistory > mwhs = new ArrayList<>( members.size() ) ;
for( Member member : members )
{
Optional< History > historyOptional = histories.stream().filter( history -> history.memberId.equals( member.id ) ).findAny() ;
if( historyOptional.isPresent() )
{
MemberWithHistory mwh = new MemberWithHistory( member , historyOptional.get() ) ;
mwhs.add( mwh ) ;
}
}
If you had a large number of items, searching by way of a stream repeatedly might become inefficient. Sorting and possibly deleting from a copy of the histories might be more efficient. But I would not bother for small data size or occasional use.

How to recall some values that are printed in Anylogic Console and store them?

I am creating an agent based model in Anylogic 8.7. I created a collection with ArrayList class and Agent elements using this code to separate some agents meeting a specific condition:
collection.addAll(findAll(population,p -> p.counter==variable); for (AgentyType p: collection ) { traceln(p.probability); }
The above code will store the probability attribute of the separated agents in the console. Is there a way to define a loop to retrieve the printed probability attributes from the console one by one and store them in a variable to operate on them? Or if there is a more efficient and optimized way of doing this I would be glad if you share this with me. Thank you all.
I am not sure why you are following this methodology... Agent-Based Modeling already "stores" the parameters you are looking for, you do not need the console as an intermediate. I believe what you are trying to do is the following:
for( AgentType p : agentTypes)
{
if( p.track == 1 )
{
sum = sum + p.probability * p.impact ;
}
{
I recommend you read:
https://help.anylogic.com/topic/com.anylogic.help/html/code/for.html?resultof=%22%66%6f%72%22%20%22%6c%6f%6f%70%22%20
and
https://help.anylogic.com/topic/com.anylogic.help/html/agentbased/statistics.html?resultof=%22%73%74%61%74%69%73%74%69%63%73%22%20%22%73%74%61%74%69%73%74%22%20
The latter will give you a better idea on how to collect Agent statistics based on certain criteria.
depending on the operations you want to perform you can use the following:
https://help.anylogic.com/index.jsp?topic=%2Fcom.anylogic.help%2Fhtml%2Fjavadoc%2Fcom%2Fanylogic%2Fengine%2FUtilitiesCollection.html&resultof=%22%75%74%69%6c%69%74%69%65%73%22%20%22%75%74%69%6c%22%20
you can use something like this to collect one by one the values of your probabilities.
collection.addAll(findAll(population,p -> p.counter==variable);
LinkedList <Double> probabilities= new LinkedList();
for (AgentyType p: collection ) {
probabilities.add(p.probability);
}

Is it good to use a big set or a equivalent map with sets in it?

I have some data points collected from different companies identified by companyId, and the name property of each data point could be duplicate in one company or among different companies.The problem is to group all the data points by its name property which belong to different companies, which means we ignore the data point if its company has already existed in the group.
For example the data points are:
companyId data-point name
1---------------------A
1---------------------A
1---------------------B
2---------------------A
3---------------------B
The results would be:
data-point name group
A=================(1,A)(2,A)
B=================(1,B)(2,B)
We can see that the second data point A from company 1 was ignored.
There are two ways as far as i know to do deduplicate work.
1.Build a Map<String(data point name), Set<Long(companyId)>>
Map<String, Set<Long>> dedup = new HashMap<>();
for(DataPoint dp : datapoints){
String key = dp.getName();
if(!dedup.contains(key)){
dedup.put(key, new HashSet<Long>());
}
if(dedup.get(key).contains(dp.getCompanyId()){
continue;
}
dedup.get(key).add(dp.getCompanyId());
}
2.Build a Big Set<String>
Set<String> dedup;
for(DataPoint dp : datapoints){
String key = dp.getName() + dp.getCompanyId();
if(dedup.contains(key)){
continue;
}
dedup.add(key);
}
So which one is better or more appropriate ?
Method (1) is way better, because method 2 kind of destroys the type information.
There are ready-made collections already available for such cases if you want a well-tested robust implementation, with many additional features.
Guava: https://google.github.io/guava/releases/21.0/api/docs/com/google/common/collect/HashMultimap.html
Eclipse collections:
https://www.eclipse.org/collections/
If you just want a simple implementation, you can follow your method (1) and do it yourself.
Result would be something like this:
{
"A": [1, 2],
"B": [1, 2]
}
Few reasons why I don't prefer method 2:
The method is not reliable. If company name ends with a number, then you might have false deduplication. So, you may need to add a special character like so: <id>~<name>
If you need to consider one more parameter later, it becomes more messy. You may have to do <id>~<name>~<pincode> etc.,
In method 1, you have the added convenience that you can put the company bean directly, if you implement a hashcode and equals which are based on the companyId field alone
The easiest way to do (1) would be:
Map<String, Set<Long>> dedup =
datapoints.stream().collect(
groupingBy(
DataPoint::getName,
mapping(DataPoint::getCompanyId, toSet()));
The easiest way to do (2) would be:
Set<String> dedup =
datapoints.stream()
.map(d -> d.getName() + d.getCompanyId())
.collect(toSet());
The one you choose depends upon what you're trying to do, since they yield different types of data, as well as potentially different results.

Categories

Resources