java stream Collectors.groupingBy() multiple fields - java

Stream<Map.Entry<String, Long>> duplicates = notificationServiceOrderItemDto.getService()
.getServiceCharacteristics()
.stream()
.collect(
Collectors.groupingBy(
ServiceCharacteristicDto::getName, Collectors.counting()
)
)
.entrySet()
.stream()
.filter(e -> e.getValue() > 1);
Optional<String> dupName = duplicates.map(Map.Entry::getKey).findFirst();
works perfect. But I wold like to find duplicates not just with name but also name + value + key
That means if name + value + key is the same this is duplicate.
I am looking Collectors.groupingBy()
http://www.technicalkeeda.com/java-8-tutorials/java-8-stream-grouping
but I can not find correct solution

Following works for me:
public class Groupingby
{
static class Obj{
String name;
String value;
String key;
Obj(String name, String val, String key)
{
this.name = name;
this.value = val;
this.key = key;
}
}
public static void main(String[] args)
{
List<Obj> objects = new ArrayList<>();
objects.add(new Obj("A", "K", "Key1"));
objects.add(new Obj("A", "K", "Key1"));
objects.add(new Obj("A", "X", "Key1"));
objects.add(new Obj("A", "Y", "Key2"));
Map<List<String>, Long> collected = objects.stream().collect(Collectors.groupingBy(x -> Arrays.asList(x.name, x.value, x.key), Collectors.counting()));
System.out.println(collected);
}
}
// Output
// {[A, K, Key1]=2, [A, Y, Key2]=1, [A, X, Key1]=1}
Note that I am using list of attributes for grouping by, not string concatenation of attributes. This will work with non-string attributes as well.
If you are doing string concatenation, you may have some corner cases like attributes (A, BC, D) and (AB, C, D) will result in same string.

Instead of
.collect(Collectors.groupingBy(ServiceCharacteristicDto::getName, Collectors.counting()))
you can write
.collect(Collectors.groupingBy(s->s.getName()+'-'+s.getValue()+'-'+s.getKey(), Collectors.counting()))

You can replace ServiceCharacteristicDto::getName with:
x -> x.getName() + x.getValue() + x.getKey()
Use a lambda instead of a method reference.
But also think of what findFirst would actually mean here... you are collecting to a HashMap that has no encounter order, streaming its entries and getting the first element - whatever that is. You do understand that this findFirst can give different results on different input, right? Even re-shuffling the HashMap could return you a different findFirst result.
EDIT
to get away from possible un-intentional duplicates because of String concat, you could use:
x -> Arrays.asList(x.getName(), x.getValue(), x.getKey())

Related

Group strings into multiple groups when using stream groupingBy

A simplified example of what I am trying to do:
Suppose I have a list of strings, which need to be grouped into 4 groups according to a condition if a specific substring is contained or not. If a string contains Foo it should fall in the group FOO, if it contains Bar it should fall in the group BAR, if it contains both it should appear in both groups.
List<String> strings = List.of("Foo", "FooBar", "FooBarBaz", "XXX");
A naive approach for the above input doesn't work as expected since the string is grouped into the first matching group:
Map<String,List<String>> result1 =
strings.stream()
.collect(Collectors.groupingBy(
str -> str.contains("Foo") ? "FOO" :
str.contains("Bar") ? "BAR" :
str.contains("Baz") ? "BAZ" : "DEFAULT"));
result1 is
{FOO=[Foo, FooBar, FooBarBaz], DEFAULT=[XXX]}
where as the desired result should be
{FOO=[Foo, FooBar, FooBarBaz], BAR=[FooBar, FooBarBaz], BAZ=[FooBarBaz], DEFAULT=[XXX]}
After searching for a while I found another approach, which comes near to my desired result, but not quite fully
Map<String,List<String>> result2 =
List.of("Foo", "Bar", "Baz", "Default").stream()
.flatMap(str -> strings.stream().filter(s -> s.contains(str)).map(s -> new String[]{str.toUpperCase(), s}))
.collect(Collectors.groupingBy(arr -> arr[0], Collectors.mapping(arr -> arr[1], Collectors.toList())));
System.out.println(result2);
result2 is
{BAR=[FooBar, FooBarBaz], FOO=[Foo, FooBar, FooBarBaz], BAZ=[FooBarBaz]}
while this correctly groups strings containing the substrings into the needed groups, the strings which doesn't contain the substrings and therefore should fall in the default group are ignored. The desired result is as already mentioned above (order doesn't matter)
{BAR=[FooBar, FooBarBaz], FOO=[Foo, FooBar, FooBarBaz], BAZ=[FooBarBaz], DEFAULT=[XXX]}
For now I'm using both result maps and doing an extra:
result2.put("DEFAULT", result1.get("DEFAULT"));
Can the above be done in one step? Is there a better approach better than what I have above?
This is ideal for using mapMulti. MapMulti takes a BiConsumer of the streamed value and a consumer.
The consumer is used to simply place something back on the stream. This was added to Java since flatMaps can incur undesirable overhead.
This works by can building a String array as you did before of Token and the containing String and collecting (also as you did before). If the key was found in the string, accept a String array with it and the containing string. Otherwise, accept a String array with the default key and the string.
List<String> strings =
List.of("Foo", "FooBar", "FooBarBaz", "XXX", "YYY");
Map<String, List<String>> result = strings.stream()
.<String[]>mapMulti((str, consumer) -> {
boolean found = false;
String temp = str.toUpperCase();
for (String token : List.of("FOO", "BAR",
"BAZ")) {
if (temp.contains(token)) {
consumer.accept(
new String[] { token, str });
found = true;
}
}
if (!found) {
consumer.accept(
new String[] { "DEFAULT", str });
}
})
.collect(Collectors.groupingBy(arr -> arr[0],
Collectors.mapping(arr -> arr[1],
Collectors.toList())));
result.entrySet().forEach(System.out::println);
prints
BAR=[FooBar, FooBarBaz]
FOO=[Foo, FooBar, FooBarBaz]
BAZ=[FooBarBaz]
DEFAULT=[XXX, YYY]
Keep in mind that streams are meant to make your coding world easier. But sometimes, a regular loop using some Java 8 constructs is all that is needed. Outside of an academic exercise, I would probably do the task like so.
Map<String,List<String>> result2 = new HashMap<>();
for (String str : strings) {
boolean added = false;
String temp = str.toUpperCase();
for (String token : List.of("FOO","BAR","BAZ")) {
if(temp.contains(token)) {
result2.computeIfAbsent(token, v->new ArrayList<>()).add(str);
added = true;
}
}
if (!added) {
result2.computeIfAbsent("DEFAULT", v-> new ArrayList<>()).add(str);
}
}
Instead of operating with strings "Foo", "Bar", etc. and their corresponding uppercase versions, it would be more convenient and cleaner to define an enum.
Let's call it Keys:
public enum Keys {
FOO("Foo"), BAR("Bar"), BAZ("Baz"), DEFAULT("");
private static final Set<Keys> nonDefaultKeys = EnumSet.range(FOO, BAZ); // Set of enum constants (not includes DEFAULT), needed to avoid creating EnumSet or array of constants via `values()` at every invocation of getKeys()
private String keyName;
Keys(String keyName) {
this.keyName = keyName;
}
public static List<String> getKeys(String str) {
List<String> keys = nonDefaultKeys.stream()
.filter(key -> str.contains(key.keyName))
.map(Enum::name)
.toList();
// if non-default keys not found, i.e. keys.isEmpty() - return the DEFAULT
return keys.isEmpty() ? List.of(DEFAULT.name()) : keys;
}
}
It has a method getKeys(String) which expects a string and returns a list of keys to which the given string should be mapped.
By using the functionality encapsulated in the Keys enum we can create a map of strings split into groups which correspond to the names of Keys-constants by using collect(supplier,accumulator,combiner).
main()
public static void main(String[] args) {
List<String> strings = List.of("Foo", "FooBar", "FooBarBaz", "XXX");
Map<String, List<String>> stringsByGroup = strings.stream()
.collect(
HashMap::new, // mutable container - which will contain results of mutable reduction
(Map<String, List<String>> map, String next) -> Keys.getKeys(next)
.forEach(key -> map.computeIfAbsent(key, k -> new ArrayList<>()).add(next)), // accumulator function - defines how to store stream elements into the container
(left, right) -> right.forEach((k, v) ->
left.merge(k, v, (oldV, newV) -> { oldV.addAll(newV); return oldV; }) // combiner function - defines how to merge container while executing the stream in parallel
));
stringsByGroup.forEach((k, v) -> System.out.println(k + " -> " + v));
}
Output:
BAR -> [FooBar, FooBarBaz]
FOO -> [Foo, FooBar, FooBarBaz]
BAZ -> [FooBarBaz]
DEFAULT -> [XXX]
A link to Online Demo

How to find the most frequent letters and the number of occurrences, using Stream API?

I have List with single letters inside. I need to count all duplicates and find the most frequent duplicate. The list generates randomly, so it may contain several most frequent letters.
Is it possible to create only one map inside one Stream or put the second map inside Stream? I need only one Stream chain, using the method groupingBy().
public static void mostFrequentlyDuplicateLetters(List<String> letters) {
Map<String, Long> collect = letterList
.stream()
.collect(Collectors.groupingBy(String::valueOf, Collectors.counting()))
.entrySet()
.stream()
.filter(// How to find the most frequent letters and put them on a map?))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}
First of all what you need as your return type is actually Map.Entry, not the whole Map, since you just want an entry with highest number of occurrences.
You can try this way:
public Map.Entry<String,Long> mostFrequentlyDuplicateLetters(List<String> letterList){
return letterList
.stream()
.collect(Collectors.groupingBy(String::valueOf, Collectors.counting()))
.entrySet()
.stream().max(Map.Entry.comparingByValue())
.get();
}
it may contain several most frequent letters
The solution below allows to determine all the letters from the given list that have a maximum frequency.
public static void main(String[] args) {
Map<String, Long> frequencies = getFrequencyMap(List.of("A", "B", "B", "C", "C", "C", "B", "D"));
long max = getMaxFrequency(frequencies);
System.out.println("max = " + max);
System.out.println(mostFrequentlyDuplicateLetters(frequencies, max));
}
public static Map<String, Long> getFrequencyMap(List<String> letters) {
return letters.stream()
.collect(Collectors.groupingBy(UnaryOperator.identity(), Collectors.counting()));
}
public static long getMaxFrequency(Map<String, Long> frequencies) {
return frequencies.values().stream()
.mapToLong(Long::longValue)
.max()
.orElse(0);
}
public static List<String> mostFrequentlyDuplicateLetters(Map<String, Long> frequencies,
long frequency) {
return frequencies.entrySet().stream()
.filter(entry -> entry.getValue() == frequency)
.map(Map.Entry::getKey)
.collect(Collectors.toList());
}
output (both letters B and C have frequency = 3)
max = 3
[B, C]
Is it possible to create only one map inside one Stream
If you want your code to be both efficient and clean the answer is NO. By trying to fuse more than one concern in one method you'll violate the first of the SOLID principles - The single responsibility principle.

Given a list of strings, is it feasible to get in one line a map from each length to the set of strings having that length, sorted by length?

I came up with the following, which does work, but I feel like there should be a cleaner one-line way that doesn't rely on an "outer" map (result below):
public TreeMap<Integer, HashSet<String>> mapLenToString(List<String> strings){
TreeMap<Integer, HashSet<String>> result = new TreeMap<>();
strings.stream()
.forEach(s -> {
int len = s.length();
if (result.containsKey(len)) {
HashSet<String> larger = result.get(len);
larger.add(s);
result.replace(len, larger);
}
else {
HashSet<String> newSet = new HashSet<>();
newSet.add(s);
result.put(len, newSet);
}
});
return result;
}
Just use the groupingBy collector. You can control the type of map and the type of set that the elements are collected into.
TreeMap<Integer, HashSet<String>> result = strs
.stream()
.collect(
Collectors.groupingBy(
s -> s.length(),
TreeMap::new,
Collectors.toCollection(HashSet::new)
)
);
Using this
List<String> strs = Arrays.asList(
"DEF", "ABC", "Hello world", "z",
"a", "q", "90", "12345678910", "ab");
output is
{1=[a, q, z], 2=[90, ab], 3=[ABC, DEF], 11=[12345678910, Hello world]}
Link to repl.it
You can also use Collectors.toSet() instead of toCollection(HashSet::new), where the default implementation is HashSet (but probably not guaranteed to be so)
An alternative using Guava Multimaps:
com.google.common.collect.Multimaps.index(iterable, function)-
Multimap<Integer, String> index =
Multimaps.index(strs, s -> s.length());
It's not sorted by the result, unfortunately.

Swapping key from a Map<Key, List<Values>>

I'm searching a solution for this problem(it is for an exam):
I have a Map < String, SortedSet < String > > operators populated by a function
public void addOperator(String operatorName, String... destinationNames) throws ProposalException {
if(operators.containsKey((operatorName))){
throw new ProposalException("Operator " + operatorName + "already into system!");
}
else{
SortedSet<String> destinationstemp=new TreeSet<>();
for(String s: destinationNames){
if(s!=null){
destinationstemp.add(s);
}
}
operators.put(operatorName, destinationstemp);
}
Now, i want to create a new Map < String, SortedSet < String > > destinations that has as key the destinationName and as values the operatorNames related.
How can i make this out?
P.S: this one up there is the usage of the methods and the not-in-code part is the output wanted. Sorry for the bad formattation of the code. ph is the instance of the façade pattern class
public SortedSet<String> getDestOperators(String destinationName) {...}//method that returns the **destinations** values related to destinationName}
ph.addOperator("op3","london","rome");
ph.addOperator("op2","london","berlin");
ph.addOperator("op5","berlin","rome","madrid");
ph.addOperator("op1","london","madrid","berlin");
ph.addOperator("op10","rome");
ph.addOperator("op4","madrid","berlin");
System.out.println(ph.getDestOperators("madrid"));
Output: [op1, op4, op5]
you need to go through each entry in your map and check if inner set contains the value you are checking against,
public SortedSet<String> getDestOperators(String destinationName) {
Set<String> result = new HashSet<String>();
for(Map.Entry<String,Set<String>> entry : operators.getValues()){
if(entry.getValue().contains(destinationName)){
results.add(entry.getKey());
}
}
return result;
}
To get your example output a simple one-liner with streams:
List<String> result = operators.entrySet().stream().filter(entry -> entry.getValue().contains(destinationName)).map(Entry::getKey).sorted().collect(Collectors.toList());
or here for better readability spread over multiple lines:
List<String> result = operators
.entrySet()
.stream()
.filter(entry -> entry.getValue().contains(destinationName))
.map(Entry::getKey)
.sorted()
.collect(Collectors.toList());
A more complex one-liner if you want to "reverse" the mapping as described in your text:
Map<String, List<String>> result = operators.entrySet().stream().flatMap(entry -> entry.getValue().stream().collect(Collectors.toMap(Function.identity(), o -> Arrays.asList(entry.getKey()))).entrySet().stream()).collect(Collectors.toMap(Entry::getKey, Entry::getValue, (a, b) -> Stream.of(a, b).flatMap(List::stream).sorted().collect(Collectors.toList())));
or here for better readability spread over multiple lines:
Map<String, List<String>> result2 = operators
.entrySet()
.stream()
.flatMap(entry -> entry
.getValue()
.stream()
.collect(Collectors.toMap(Function.identity(),
o -> Arrays.asList(entry.getKey())))
.entrySet()
.stream())
.collect(Collectors.toMap(Entry::getKey,
Entry::getValue,
(a, b) -> Stream.of(a, b)
.flatMap(List::stream)
.sorted()
.collect(Collectors.toList())));
What you need to do, is loop over each operator, and then loop over all entries in the list, if value from the list is not yet present in your output map, you add it, else you modify its colection of operators.
Here is some code for you:
origin.forEach((key, list) -> {list.forEach(city -> {
if(result.containsKey(city))
result.get(city).add(key);
else{
SortedSet<String> set = new TreeSet<>();
set.add(key);
result.put(city, set);
});
});

Java 8 stream sum entries for duplicate keys

I am using Java 8 streams to group a list of entries by a certain key and then sorting the groups by date. What I would like to do in addition is to "collapse" any two entries within a group that have the same date and sum them up. I have a class like this (stripped down for example purposes)
class Thing {
private String key;
private Date activityDate;
private float value;
...
}
Then I'm grouping them like so:
Map<String, List<Thing>> thingsByKey = thingList.stream().collect(
Collectors.groupingBy(
Thing::getKey,
TreeMap::new,
Collectors.mapping(Function.identity(), toSortedList())
));
private static Collector<Thing,?,List<Thing>> toSortedList() {
return Collectors.collectingAndThen(toList(),
l -> l.stream().sorted(Comparator.comparing(Thing::getActivityDate)).collect(toList()));
}
What I would like to do is, if any two Thing entries have the exact same date, sum up the values for those and collapse them down such that,
Thing1
Date=1/1/2017
Value=10
Thing2
Date=1/1/2017
Value=20
Turns into 30 for 1/1/2017.
What's the best way to accomplish something like that?
I have slightly change your Thing class to use LocalData and added a very simple toString:
#Override
public String toString() {
return " value = " + value;
}
If I understood correctly, than this is what you need:
Map<String, TreeMap<LocalDate, Thing>> result = Arrays
.asList(new Thing("a", LocalDate.now().minusDays(1), 12f), new Thing("a", LocalDate.now(), 12f), new Thing("a", LocalDate.now(), 13f))
.stream()
.collect(Collectors.groupingBy(Thing::getKey,
Collectors.toMap(Thing::getActivityDate, Function.identity(),
(Thing left, Thing right) -> new Thing(left.getKey(), left.getActivityDate(), left.getValue() + right.getValue()),
TreeMap::new)));
System.out.println(result); // {a={2017-06-24= value = 12.0, 2017-06-25= value = 25.0}}
This can be accomplished using the toMap collector:
Map<Date, Thing> thingsByDate = things.stream().collect(Collectors.toMap(
Thing::getActivityDate,
Function.identity(),
(thing1, thing2) -> new Thing(null, thing1.getActivityDate(), thing1.getValue()+thing2.getValue())
);
You may then do with this map as you wish.

Categories

Resources