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.
Related
I have a Map <Long, List<SerialDate>>:
The SerialDate has two fields: Number,String
1 -> (Number1|String1, Number2|String2)
2 -> (Number1|String1, Number2|String2)
3 -> (Number3|String3)
The code is like:
Map<Long, List<SerialMaskData>> upcDataMap = new HashMap<>();//
for(SerialMappingData data : serialDataList3p){
Long upc = data.getUpcNumber();
String mask = data.getSerialMask();
String algorithm = data.getSerialAlgorithm();//
SerialMaskData serialMaskData = new SerialMaskData(mask,algorithm);//
if(!upcDataMap.containsKey(upc)){
List<SerialMaskData> list = new ArrayList<>();//
list.add(serialMaskData);//
upcDataMap.put(upc, list);
}else{
upcDataMap.get(upc).add(serialMaskData);
}
}
What I want is to do a classification:
[1,2], (Number1|String1, Number2|String2), groupId1 // because they have the same value format.
[3], (Number3|String3), groupId2
I have tried to build a reverse Map<List<Number|String>, Long>, but it was a bad idea to use List as a map key. I think it can be done with stream.groupingby but not sure how to do it.
You have a Map<Long,List<SerialDate>>, and for the described grouping, you would like to have a Map<List<SerialDate>,List<Long>>, right?
As you already found out yourself, having an instance of List as the key for a Map is not the best idea …
But if SerialDate provides a proper implementation of Object::toString, you can get something equivalent that should be sufficient for your purpose.
Try this:
final Map<Long,List<SerialDate>> input = …
final Map<String,List<Long>> output = new HashMap<>();
for( final var entry : input.entrySet() )
{
final var key = entry.getValue()
.stream()
.map( SerialDate::toString )
.sorted()
.collect( joining() );
final var values = output.computeIfAbsent( key, k -> new ArrayList<Long>() );
values.add( entry.getKey();
}
Basically, the code converts the list into a String, and uses that String as the key for the output map. The sorting is required because two lists are (assumed to be) semantically equal if they contain the same elements, independent from their order, while for Strings the order of their 'elements' is crucial for equality.
Simply calling toString() on the List instance does not work as it will not call toString() on the element.
I have a simple pojo:
class Pojo
{
string key;
int value;
}
This is in a list:
List<Pojo> myList;
I can have multiple values per key, so for example:
A 5
A 7
B 3
So I want to group by the key and sum the values and put it back into a List<Pojo> with a single entry for A 12, B 3, etc.
This is what I have right now:
myMap.get("xxx")
.values()
.stream()
.collect(Collectors.groupingBy(Pojo::getKey), Collectors.summingLong(Pojo::getValue))
This gets me a Map<String, Int>.
Do I need to do something like:
myMap.get("xxx")
.values()
.stream()
.collect(Collectors.groupingBy(Pojo::getKey), Collectors.summingLong(Pojo::getValue))
.entrySet().stream.map((entry) -> new Pojo(..)).collect(Collectors.toList())
Or is there a cleaner way to do it?
There are several ways to do it. Sometimes the simplest is the best and most efficient. Here is one of them.
List<Pojo> list = List.of(new Pojo("A", 5), new Pojo("B", 3),
new Pojo("A", 7), new Pojo("C", 10), new Pojo("B", 12));
Map<String, Pojo> map = new HashMap<>();
The way this works is as follows:
it requires a simple setter in your class to set the value.
Iterate thru the list and if the map doesn't contain it, add it.
else use the just retreived object and add its value and the current object's value and set the sum in the Map's version of the object. So the map maintains a tally of the update without replacing the object in the map.
for (Pojo p : list) {
Pojo cp;
if ((cp = map.get(p.getKey())) != null) {
cp.setValue(cp.getValue() + p.getValue());
} else {
map.put(p.getKey(), p);
}
}
This next step could be eliminated if you don't mind working with type Collection.
List<Pojo> results = new ArrayList<>(map.values());
Now print them
results.forEach(Sytem.out::println);
Prints
A 12
B 15
C 10
On the other hand, if you prefer streams this works sans a setter. But it does create a new object to replace the old one.
Map<String, Pojo> map = list.stream()
.collect(Collectors.toMap(Pojo::getKey, s -> s,
(a, b) -> new Pojo(a.getKey(),
a.getValue() + b.getValue())));
map.values().forEach(Sytem.out::println);
The pojo class
class Pojo {
String key;
int value;
public Pojo(String key, int value) {
this.key = key;
this.value = value;
}
public int getValue() {
return value;
}
public String getKey() {
return key;
}
public void setValue(int v) {
this.value = v;
}
public String toString() {
return key + " " + value;
}
}
It should work when using Collectors::reducing.
myMap.get("xxx").values().stream()
.collect(Collectors.groupingBy(Pojo::getKey, Collectors.reducing(
new Pojo(null, 0),
(a, b) -> new Pojo(b.getKey(), a.getValue() + b.getValue())))
.values()
The call to groupingBy would return a Map<String, List<Pojo>>, but providing a reducing collector as second argument to groupingBy takes the values of the list and reduces them to a single element. The result is a Map<String, Pojo>, and in order to get a Collection<Pojo>, we simply need to call values() on the Map.
I have a simple list of strings. My goal is to get the last occurrences of each string in the list by group.
This is mode code:
List<String> newData = new ArrayList<>();
newData.add("A-something");
newData.add("A-fdfdsfds");
newData.add("A-fdsfdsfgs");
newData.add("B-something");
newData.add("B-dsafdrsafd");
newData.add("B-dsdfsad");
I wish to get only the last occurrence of each group. In other words I wanst to get "A-fdsfdsfgs" and "B-dsdfsad" only.
How to do so?
To get last occurrences for each group you can use stream api with groupingBy:
import static java.util.stream.Collectors.*;
Map<String, Optional<String>> collect = newData.stream()
.collect(groupingBy(strings -> strings.split("-")[0],
mapping(s -> s, maxBy(Comparator.comparingInt(newData::lastIndexOf)))));
Note: map has Optional as a value
To get it without Optional use toMap instead of groupingBy:
Map<String, String> collect = newData.stream()
.collect(toMap(s -> s.split("-")[0],
Function.identity(),
(s1, s2) -> newData.lastIndexOf(s1) > newData.lastIndexOf(s2) ? s1 : s2));
Also if you want to have map values without group name, then change Function.identity() with s -> s.split("-")[1]
import java.util.*;
class Solution {
public static void main(String[] args) {
List<String> newData = new ArrayList<>();
newData.add("A-something");
newData.add("A-fdfdsfds");
newData.add("A-fdsfdsfgs");
newData.add("B-something");
newData.add("B-dsafdrsafd");
newData.add("B-dsdfsad");
System.out.println(lastOccurrences(newData).toString());
}
private static List<String> lastOccurrences(List<String> data){
Set<String> set = new HashSet<>();
List<String> ans = new ArrayList<>();
for(int i=data.size()-1;i>=0;--i){
String group = data.get(i).substring(0,data.get(i).indexOf("-"));
if(set.contains(group)) continue;
set.add(group);
ans.add(data.get(i));
}
return ans;
}
}
Output:
[B-dsdfsad, A-fdsfdsfgs]
Algorithm:
Move from last to first, instead of first to last because you want last occurrences. This will make the management easier and code a little bit clean.
Get the group the string belongs to using substring() method.
Use a set to keep track of already visited groups.
If a group is not in the set, add it to the set and current string to our answer(since this will be the last occurred) for this group.
Finally, return the list.
There are several ways to this, as the other answers already show. I’d find something like the following natural:
Collection<String> lastOfEach = newData.stream()
.collect(Collectors.groupingBy((String s) -> s.split("-")[0],
Collectors.reducing("", s -> s, (l, r) -> r)))
.values();
lastOfEach.forEach(System.out::println);
With your list the output is:
A-fdsfdsfgs
B-dsdfsad
My grouping is the same as in a couple of other answers. On the grouped values I perform a reduction, each time I got two strings taking the latter of them. In the end this will give us the last string from each group as requested. Since groupingBy produces a map, I use values to discard the keys ( A and B) and get only the original strings.
Collecting via grouping should be sufficient.
final Map<String, List<String>> grouped =
newData.stream()
.collect(groupingBy(s -> s.split("-")[0]));
final List<String> lastOccurrences =
grouped.values()
.stream()
.filter(s -> !s.isEmpty())
.map(s -> s.get(s.size() - 1))
.collect(toList());
For Java 11, the filter becomes filter(not(List::isEmpty))
This will give you fdsfdsfgs, dsdfsad
Using a temporary Map. The List finalList will have only the required values
Map<String, String> tempMap = new HashMap<>();
List<String> finalList = new ArrayList<>();
newData.forEach((val) -> tempMap.put(val.split("-")[0], val.split("-")[1]));
tempMap.forEach((key, val) -> finalList.add(key + "-" + val));
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())
I have a Multimap structure, Map<String, Set<String>> as input. I want to group entries of this map if any two sets of entry values have a common element. Output should be of the format Map<Set<String>, Set<String>> where each key will be a group of keys from the input map.
eg. given this input:
A -> [1,2]
B -> [3,4]
C -> [5,6]
D -> [1,5]
Output:
[A,C,D] -> [1,2,5,6]
[B] -> [3,4]
Here A & D have 1 as common element, C & D have 5 as common element. So A, C, D are merged into one key.
There are lots of ways you can solve this. One that I like (assuming you are using Java 8) is to implement this as a collector for a Map.Entry stream. Here's a possible implementation:
public class MapCollector {
private final Map<Set<String>,Set<Integer>> result = new HashMap<>();
public void accept(Map.Entry<String,Set<Integer>> entry) {
Set<String> key = new HashSet<>(Arrays.asList(entry.getKey()));
Set<Integer> value = new HashSet<>(entry.getValue());
Set<Set<String>> overlapKeys = result.entrySet().stream()
.filter(e -> e.getValue().stream().anyMatch(value::contains))
.map(Map.Entry::getKey)
.collect(Collectors.toSet());
overlapKeys.stream().forEach(key::addAll);
overlapKeys.stream().map(result::get).forEach(value::addAll);
result.keySet().removeAll(overlapKeys);
result.put(key, value);
}
public MapCollector combine(MapCollector other) {
other.result.forEach(this::accept);
return this;
}
public static Collector<Map.Entry<String, Set<Integer>>, MapCollector, Map<Set<String>,Set<Integer>>> collector() {
return Collector.of(MapCollector::new, MapCollector::accept, MapCollector::combine, c -> c.result);
}
}
This can be used as follows:
Map<Set<String>,Set<Integer>> result = input.entrySet().stream()
.collect(MapCollector.collector());
Most of the work is done in the accept method. It finds all overlapping sets and moves them to the new map entry. It supports parallel streams which could be useful if your map is massive.