how to use java stream to group fields and create a summary - java

public class Call {
private String status;
private String callName;
}
I have a list of calls and i have to create a summary, like this:
public class CallSummary {
private String callName;
private List<ItemSummary> items;
}
public class itemSummary {
private String status;
private Integer percentage;
}
My goal is show a percentage of calls with some status
like :
INBOUND_CALL : {
FAILED = 30%
SUCCESS = 70%
}
how can i do it using java 8 stream and Collectors ?

The idea behind the grouping would be to nest is in such a way that you have a call name and then status based count lookup available. I would also suggest using an enumeration for the status
enum CallStatus {
FAILED, SUCCESS
}
and adapting it in other classes as
class Call {
private CallStatus status;
private String callName;
}
Then you can implement a nested grouping and start off with an intermediate result such as:
List<Call> sampleCalls = List.of(new Call(CallStatus.SUCCESS,"naman"),new Call(CallStatus.FAILED,"naman"),
new Call(CallStatus.SUCCESS,"diego"), new Call(CallStatus.FAILED,"diego"), new Call(CallStatus.SUCCESS,"diego"));
Map<String, Map<CallStatus, Long>> groupedMap = sampleCalls.stream()
.collect(Collectors.groupingBy(Call::getCallName,
Collectors.groupingBy(Call::getStatus, Collectors.counting())));
which would give you an output of
{diego={FAILED=1, SUCCESS=2}, naman={FAILED=1, SUCCESS=1}}
and you can further evaluate the percentages as well. (though representing them in Integer might lose precision depending on how you evaluate them further.)
To solve it further, you can keep another Map for the name-based count lookup as:
Map<String, Long> nameBasedCount = calls.stream()
.collect(Collectors.groupingBy(Call::getCallName, Collectors.counting()));
and further, compute summaries of type CallSummary in a List as :
List<CallSummary> summaries = groupedMap.entrySet().stream()
.map(entry -> new CallSummary(entry.getKey(), entry.getValue().entrySet()
.stream()
.map(en -> new ItemSummary(en.getKey(), percentage(en.getValue(),
nameBasedCount.get(entry.getKey()))))
.collect(Collectors.toList()))
).collect(Collectors.toList());
where percentage count be implemented by you using the signature int percentage(long val, long total) aligned with the datatype chosen in ItemSummary as well.
Sample result:
[
CallSummary(callName=diego, items=[ItemSummary(status=FAILED, percentage=33), ItemSummary(status=SUCCESS, percentage=66)]),
CallSummary(callName=naman, items=[ItemSummary(status=FAILED, percentage=50), ItemSummary(status=SUCCESS, percentage=50)])
]

The following collects to a status -> percent map which you can then convert to you output model. This code assumes a getStatus method.
List<Call> calls;
Map<String,Double> statusPercents = calls.stream()
.collect(Collectors.groupingBy(Call::getStatus,
Collectors.collectingAndThen(Collectors.counting(),
n -> 100.0 * n / calls.size())));
I realise this code is a bit hard to read. The chain of collectors group the calls by status and then counts each group and finally converts to a percent. You could (arguably) make it more readable by having interim variables for the collectors:
var percentFunction = n -> 100.0 * n / calls.size();
var collectPercent = collectingAndThen(count(), percentFunction);
var collectStatusPercentMap = groupingBy(Call::getStatus, collectPercent);
You also want to group by call name but that's really just the same thing - using groupingBy and then reducing the list of calls to a CallSummary.

Related

Filter an object by specific properties java

So I have this class:
public class Seat {
private Long id;
private float positionX;
private float positionY;
private int numOfSeats;
private String label;
//getters and setters
}
I have List of Seat class on:
List<Seat> seatList = // get data from repository;
I also have this arraylist contains list of ids:
List<Long> idList; // for example : [1, 2, 3]
I want to filter seatList so that the filtered ArrayList does not contain a Seat object with id from idList, so I tried to use stream:
List<Seat> filteredSeat = seatList.stream()
.filter(seat -> {
// function to filter seat.getId() so it would return the Seat object with id that does not equals to ids from idList
})
.collect(Collectors.toList());
I cant find the correct function to do it. Does anyone have suggestion for me to try?
You want to use the overriden method from Collection#contains(Object) with the negation implying the id was not found in the List.
Set<Seat> filteredSeat = seatList.stream()
.filter(seat -> !idList.contains(seat.getId()))
.collect(Collectors.toSet());
Few notes:
You want to use Set<Long> instead of List<Long> for an efficient look-up. Moreover, it doesn't make sense to have duplicate values among ids, so Set is a good choice.
Collectors.toSet() results Set, so the Stream's return type is Set<Seat>.
The most simple solution for be a for-each loop in which you check each idList against the Seat's id.
perhaps something like
List<Seat> FilteredList;
for ( Seat CurSeat : seatList ){
for(int i = 0; i < idList.size(); i++){
and if the ID of CurSeat is part of idList, it doesn't get added to the new List.
This is definitely not the simplest way, but if you're looking for something easy, this is probably it.
Hope this helped!
Assuming you implement the equals method accordingly (like the doc mentions it), there is a much shorter solution:
seatList.stream()
.distinct()
.collect( Collectors.toList() );

JAVA: Partitioning list of user objects on a condition using Stream API and convert to Map<String,String>

I have a class Agent, which has following members:
class Agent{
String name;
long funds;
//... getters and setters, parameterized constructor
}
Now, I have a list of Agent class objects.
ArrayList<Agent> listAgents=new ArrayList<Agent>();
I want to give stars to the top performers, like 5 star for someone who has funds of more than 100000, 4 stars to someone with fund of 80000, and so on.
I want to store this record in Map<String,String> like
<ABC,*****> <PQR,****>
I tried the following code:
Map<String,String> star=listAgents
.stream()
.collect(Collectors.groupingBy(agn->giveStars(agn.getGeneratedFund())));
Here giveStars is function defined as below:
public static String giveStars(long generatedFund) {
if(generatedFund>=100000)
return "*****";
else if(generatedFund<100000&& generatedFund>=80000)
return "****";
else if(generatedFund<80000 && generatedFund>=60000)
return "***";
return "";
}
This didn't work. Also, I don't think this is the right way to do this.
How to do this using Stream API's functions?
Any help would be appreciated. Thanks in advance.
Assuming <ABC,*****> <PQR,****> means you have a map of agent name to its star rating, you don't want grouping, you want to map your existing items to different ones.
Here's one way how:
Map<String,String> star = listAgents.stream()
.collect(Collectors.toMap(Agent::getName, agn->giveStars(agn.getGeneratedFund())));
If you group items, you don't change them but assign to different groups (doh). Mapping is a process where you change an object to be something different.
First, there is an issue in the method giveStars which should be rewritten:
public static String giveStars(long generatedFund) {
if (generatedFund >= 1_000_000)
return "*****";
else if (generatedFund >=800_000)
return "****";
else if (generatedFund >= 600_000)
return "***";
return "";
}
Next, the code Collectors.groupingBy(agn->giveStars(agn.getGeneratedFund())) contradicts the intention to have a map Map<String, String> - it will create Map<String, List<Agent>> where the key is the star rating. So maybe your real intention is to change the type of the map.
Map<String, List<Agent>> groupByStars = listAgents.stream()
.collect(Collectors.groupingBy(agent -> giveStars(agent.getGeneratedFund())));
Such map will let to get top agents immediately.
If Map<String, String> (agent name to stars rating) is required, toMap collector may be used as suggested earlier, but this would work only if agent names are unique. If some duplicate values may occur, a merge function is required (e.g. to keep the agent with a better rating):
Map<String, String> groupByAgentName = listAgents.stream()
.collect(Collectors.toMap(
Agent::getName,
agent -> giveStars(agent.getGeneratedFund()),
(rating1, rating2) -> rating1.length() > rating2.length() ? rating1 : rating2,
LinkedHashMap::new // keep insertion order
));

populate a List<Object1> based on another List<Object2> using java 8 stream

I have 2 List
List<Obligation> and List<ObligationStatus>
Structure is as follows:
public class Obligation {
private String topic;
private String status;
private String comment;
private String text;
}
and
public class ObligationStatus {
private String topic;
private String status;
private String comment;
}
status and comment inside List<Obligation> is null for all the elements,
topic is populated in both the list
I want to set the status and comment from each element of List<ObligationStatus> into each element of List<Obligation> based on topic.
// this is what i have tried, and is working fine
obList.stream().forEach(ob -> {
osList.stream().forEach(os -> {
if (ob.getTopic().equalsIgnoreCase(os.getTopic())) {
ob.setStatus(os.getStatus());
ob.setComment(os.getComment());
}
});
});
// also tried to do this without using forEach, but compilation error here
List<Obligation> result = obList.stream()
.map(ob -> osList.stream().map(os -> os.getTopic().equals(ob.getTopic())).collect(Function.identity()))
.collect(Collectors.toList());
Can we do this without suing forEach ?
any info would be helpful.
Why use stream for this? Stream is not the right tool for mutating objects.
Use standard for loops. Makes code easier to understand too.
List<Obligation> obligationList = ...;
List<ObligationStatus> statusList = ...;
// For better performance, make a map
Map<String, ObligationStatus> statusMap = new HashMap<>(statusList.size());
for (ObligationStatus status : statusList)
statusMap.put(status.getTopic(), status);
// Assign status values
for (Obligation obligation : obligationList) {
ObligationStatus status = statusMap.get(obligation.getTopic());
if (status != null) {
ob.setStatus(status.getStatus());
ob.setComment(status.getComment());
}
}
If you want to do some stream logic, the first part is a good candidate:
// For better performance, make a map
Map<String, ObligationStatus> statusMap = statusList.stream()
.collect(Collectors.toMap(ObligationStatus::getTopic, Function.identity()));
UPDATE
Noticed that question code did equalsIgnoreCase(...) when comparing topic values. If that is really needed, change the HashMap to a case-insensitive TreeMap:
Map<String, ObligationStatus> statusMap = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
The stream version of that gets convoluted, so better keep it old-style.
Since you are modifying an existing List, there's no need to use collect.
You could, however, use a single forEach. Locating the ObligationStatus instance matching the Obligation instance can be done with filter and findFirst.
obList.stream().forEach(ob -> {
osList.stream()
.filter(os -> ob.getTopic().equalsIgnoreCase(os.getTopic()))
.findFirst()
.ifPresent (os -> {
ob.setStatus(os.getStatus());
ob.setComment(os.getComment());
});
});

Remake list with some condition

There are two entities:
class GiftCertificate {
Long id;
List<Tag> tags;
}
class Tag {
Long id;
String name;
}
There is a list
List<GiftCertificate>
which contains, for example, the following data:
<1, [1, "Tag1"]>, <2, null>, <1, [2, "Tag2"]>. (It does not contain a set of tags, but only one tag or does not have it at all).
I need to do so that in the result it was this:
<1, {[1," Tag1 "], [2," Tag2 "]}>, <2, null>. I mean, add to the set of the first object a tag from the third GiftCertificate and at the same time delete the 3rd one. I would like to get at least some ideas on how to do this. it would be nice to use stream.
Probably not the most effective way, but it might help
private List<GiftCertificate> joinCertificates(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.collect(Collectors.groupingBy(GiftCertificate::getId))
.entrySet().stream()
.map(entry -> new GiftCertificate(entry.getKey(), joinTags(entry.getValue()))).collect(Collectors.toList());
}
private List<Tag> joinTags(List<GiftCertificate> giftCertificates) {
return giftCertificates.stream()
.flatMap(giftCertificate -> Optional.ofNullable(giftCertificate.getTags()).stream().flatMap(Collection::stream))
.collect(Collectors.toList());
}
You can do what you want with streams and with the help of a dedicated custom constructor and a couple of helper methods in GiftCertificate. Here's the constructor:
public GiftCertificate(GiftCertificate another) {
this.id = another.id;
this.tags = new ArrayList<>(another.tags);
}
This just works as a copy constructor. We're creating a new list of tags, so that if the list of tags of either one of the GiftCertificate instances is modified, the other one won't. (This is just basic OO concepts: encapsulation).
Then, in order to add another GiftCertificate's tags to this GiftCertificate's list of tags, you could add the following method to GiftCertificate:
public GiftCertificate addTagsFrom(GiftCertificate another) {
tags.addAll(another.tags);
return this;
}
And also, a helper method that returns whether the list of tags is empty or not will come in very handy:
public boolean hasTags() {
return tags != null && !tags.isEmpty();
}
Finally, with these three simple methods in place, we're ready to use all the power of streams to solve the problem in an elegant way:
Collection<GiftCertificate> result = certificates.stream()
.filter(GiftCertificate::hasTags) // keep only gift certificates with tags
.collect(Collectors.toMap(
GiftCertificate::getId, // group by id
GiftCertificate::new, // use our dedicated constructor
GiftCertificate::addTagsFrom)) // merge the tags here
.values();
This uses Collectors.toMap to create a map that groups gift certificates by id, merging the tags. Then, we keep the values of the map.
Here's the equivalent solution, without streams:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.merge(
cert.getId(),
new GiftCertificate(cert),
GiftCertificate::addTagsFrom);
}
});
Collection<GiftCertificate> result = map.values();
And here's a variant with a slight performance improvement:
Map<Long, GiftCertificate> map = new LinkedHashMap<>(); // preserves insertion order
certificates.forEach(cert -> {
if (cert.hasTags()) {
map.computeIfAbsent(
cert.getId(),
k -> new GiftCertificate(k)) // or GitCertificate::new
.addTagsFrom(cert);
}
});
Collection<GiftCertificate> result = map.values();
This solution requires the following constructor:
public GiftCertificate(Long id) {
this.id = id;
this.tags = new ArrayList<>();
}
The advantage of this approach is that new GiftCertificate instances will be created only if there's no other entry in the map with the same id.
Java 9 introduced flatMapping collector that is particularly well-suited for problems like this. Break the task into two steps. First, build a map of gift certificate IDs to list of tags and then assemble a new list of GiftCertificate objects:
import static java.util.stream.Collectors.flatMapping;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
......
Map<Long, List<Tag>> gcIdToTags = gcs.stream()
.collect(groupingBy(
GiftCertificate::getId,
flatMapping(
gc -> gc.getTags() == null ? Stream.empty() : gc.getTags().stream(),
toList()
)
));
List<GiftCertificate> r = gcIdToTags.entrySet().stream()
.map(e -> new GiftCertificate(e.getKey(), e.getValue()))
.collect(toList());
This assumes that GiftCertificate has a constructor that accepts Long id and List<Tag> tags
Note that this code deviates from your requirements by creating an empty list instead of null in case there are no tags for a gift certificate id. Using null instead of an empty list is just a very lousy design and forces you to pollute your code with null checks everywhere.
The first argument to flatMapping can also be written as gc -> Stream.ofNullable(gc.getTags()).flatMap(List::stream) if you find that more readable.

How to groupBy and Collect with Java 8?

I have a list of elements, let's call it "keywords", like this:
public class Keyword {
Long id;
String name;
String owner;
Date createdTime;
Double price;
Date metricDay;
Long position;
}
The thing is that there is a keyword for every single day. For example:
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.1", metricDay="11/11/1999", position=109}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.3", metricDay="12/11/1999", position=108}
Keyword{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", price="0.2", metricDay="13/11/1999", position=99}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.6", metricDay="13/11/1999", position=5}
Keyword{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="14/11/1999", position=4}
Keyword{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", price="0.1", metricDay="13/11/1999", position=8}
Then, from this list I would like to create a new list with all the metrics from all those different days on one single list. First, I created a class like this:
public class KeywordMetric {
Double price;
Date metricDay;
Long position;
}
And what I would like to archive is go from the first list, to a structure like this:
public class KeywordMeged {
Long id;
String name;
String owner;
List<KeywordMetric> metricList;
}
Example of what I expect:
KeywordMerged{id=1, name="kw1", owner="Josh", createdTime="12/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="11/11/1999",position=109},KeywordMetric{price=0.3,metricDay="12/11/1999",position=108},KeywordMetric{price=0.2,metricDay="13/11/1999",position=99}]
KeywordMerged{id=2, name="kw2", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.6,metricDay="13/11/1999",position=5},KeywordMetric{price=0.1,metricDay="14/11/1999",position=4}]
KeywordMerged{id=3, name="kw3", owner="Josh", createdTime="13/12/1992", metricList=[KeywordMetric{price=0.1,metricDay="13/11/1999",position=8}]
I know how to do this with a lot of loops and mutable varibles, but I can't figure out how to do this with streams and lambda operations. I was able to group all related keywords by Id with this:
Map<Long, List<Keyword>> kwL = kwList.stream()
.collect(groupingBy(Keyword::getId))
And I know that with .forEach() I could iterate over that Map, but can't figure out how to make the collect() method of streams pass from List to KeywordMerged.
You can try to use the Collectors.toMap(...) instead. Where:
Keyword::getId is a key mapper function.
KeywordMerged.from(...) performs a transformation: Keyword => KeywordMerged
(left, right) -> { .. } combines metrics for entities with identical ids.
Collection<KeywordMerged> result = keywords.stream()
.collect(Collectors.toMap(
Keyword::getId,
k -> KeywordMerged.from(k), // you can replace this lambda with a method reference
(left, right) -> {
left.getMetricList().addAll(right.getMetricList());
return left;
}))
.values();
A transformation method might look something like this:
public class KeywordMerged {
public static KeywordMerged from(Keyword k) {
KeywordMetric metric = new KeywordMetric();
metric.setPrice(k.getPrice());
metric.setMetricDay(k.getMetricDay());
metric.setPosition(k.getPosition());
KeywordMerged merged = new KeywordMerged();
merged.setId(k.getId());
merged.setName(k.getName());
merged.setOwner(k.getOwner());
merged.setMetricList(new ArrayList<>(Arrays.asList(metric)));
return merged;
}
}
I think you've got the basic idea. So, refactor according to your needs...
A slightly different approach. First you collect the Map of keywords grouped by id:
Map<Integer, List<Keyword>> groupedData = keywords.stream()
.collect(Collectors.groupingBy(k -> k.getId()));
Further you convert your map to the list of desired format:
List<KeywordMerged> finalData = groupedData.entrySet().stream()
.map(k -> new KeywordMerged(k.getValue().get(0).getId(),
k.getValue().stream()
.map(v -> new KeywordMetric(v.getMetricDay(), v.getPrice(), getPosition()))
.collect(Collectors.toList())))
.collect(Collectors.toList());
This will work on the grouped data, but transforming the map it will create KeywordMerged object, which as argument will receive id (you can extent it further yourself) and converted to List<KeywordMetric> previously grouped by ID data.
EDIT: I believe with some extraction to methods you can make it look much nicer :)

Categories