Java 8 Stream groupingBy with custom logic - java

I have a list of Records. Which has two fields: LocalDateTime instant and a Double data.
I want to groupBy all the records by Hour and create a Map<Integer, Double>. Where the keys (Integer) are hours and values(Double) are last Data of that hour - first Data of that hour.
What I have done so far is following:
Function<Record, Integer> keyFunc = rec->rec.getInstant().getHour();
Map<Integer, List<Record>> valueMap = records.stream().collect(Collectors.groupingBy(keyFunc));
I want the value map to hold Double instead of List<Records>.
For Example: List records can be following:
Instant Data
01:01:24 23.7
01:02:34 24.2
01:05:23 30.2
...
01:59:27 50.2
02:03:23 54.4
02:04:23 56.3
...
02:58:23 70.3
...
etc
Resulting map should be:
Key Value
1 26.5 (50.2-23.7)
2 15.9 (70.3-54.4)
...

You are mostly looking for Collectors.mapping within the groupingBy.
Map<Integer, List<Double>> valueMap = records.stream()
.collect(Collectors.groupingBy(keyFunc,
Collectors.mapping(Record::getData, Collectors.toList())));
This would group Records by their instant's hour and corresponding data for such records into a List as values of the map. Based on comments further
I want to subtract the first data from the last data
Yes the list will be sorted list based on instant
you can use the grouped map to get the desired output as:
Map<Integer, Double> output = new HashMap<>();
valueMap.forEach((k, v) -> output.put(k, v.get(v.size() - 1) - v.get(0)));
Alternatively, you could use Collectors.mapping with Collectors.collectingAndThen further as:
Map<Integer, Double> valueMap = records.stream()
.collect(Collectors.groupingBy(keyFunc,
Collectors.mapping(Record::getData,
Collectors.collectingAndThen(
Collectors.toList(), recs -> recs.get(recs.size() - 1) - recs.get(0)))));

You can use collectingAndThen as a downstream collector to groupingBy, and use the two extreme values of each group to compute the difference:
Map<Integer, Double> result = records.stream()
.collect(
Collectors.groupingBy(rec -> rec.getInstant().getHour(),
Collectors.collectingAndThen(
Collectors.toList(),
list -> {
//please handle the case of 1 entry only
list.sort(Comparator.comparing(Record::getInstant));
return list.get(list.size() - 1).getData()
- list.get(0).getData();
})));
Collectors.groupingBy(rec -> rec.getInstant().getHour() will group entries by hour. As used here, Collectors.collectingAndThen will take hourly entries as lists, sort every such list by the instant field then find the difference between the two extreme elements.

Based on the comment that the list would be sorted on timestamp, the following would work
:
Map<Integer, Double> valueMap = records.stream()
.collect(Collectors.groupingBy(rec -> rec.getInstant().getHour(),
Collectors.mapping(Record::getData,
Collectors.collectingAndThen(Collectors.toList(),recs -> recs.get(recs.size()-1) - recs.get(0)))));

Related

Store multiple values for a key in Collectors.toMap() in Java [duplicate]

This question already has answers here:
Java stream/collect: map one item with multiple fields to multiple keys
(1 answer)
HashMap with multiple values under the same key
(21 answers)
Closed 1 year ago.
This is how my map looks like.
Map<String, String> maps = List.stream().collect(Collectors.toMap(Cat::getName, Cat::getNumber, (c1,c2) -> c1));
If I already have "Lucy, 101" in maps then I am unable to add another value corresponding to the name Lucy i.e "Lucy, 102". Is there any way to change the merge function [ (c1,c2) -> c1 ] so that I can have two values corresponding to a single key (Lucy) in my maps?
By your requirement, in order to allow multiple values for the same key, you need to implement the result as Map<String, Collection<String>>.
In your case, you can merely use groupingBy + mapping collectors instead of toMap with the merge function:
Map<String, List<String>> maps = List.stream()
.collect(Collectors.groupingBy(
Cat::getName,
Collectors.mapping(Cat::getNumber, Collectors.toList())
);
However, you may want to consider a merge function as joining strings:
Map<String, String> maps = List.stream()
.collect(Collectors.toMap(
Cat::getName,
Cat::getNumber, (num1, num2) -> String.join("; ", num1, num2)
);
Then the map would contain "compound" values in the form of a string.
Since the type of your map is Map<String, String>, it can only return one string. However, you expect to get multiple strings from get. You need to change the type of the map to e.g. Map<String, List<String>> or Map<String, Collection<String>>.
Then, you can use groupingBy like this:
Map<String, List<String>> map = yourList.stream().collect(Collectors.groupingBy(
Cat::getName, // group by the name
Collectors.mapping( // map each group of cats to their numbers
Cat::getNumber, Collectors.toList() // collect each group to a list
)
));
If you are okay with multiple numbers in the same string (e.g. "101 102"), you can use Collectors.joining:
Map<String, String> map = yourList.stream().collect(Collectors.groupingBy(
Cat::getName,
Collectors.mapping(
Cat::getNumber, Collectors.joining(" ")
)
));

Merge list of maps with duplicate keys [duplicate]

This question already has answers here:
Merge map of arrays with duplicate keys
(8 answers)
Merging a list of maps into a single map
(5 answers)
Closed 2 years ago.
I have a list of HashMap<Integer, ArrayList<String>> and would like to merge them in a loop.
The problem is that each map's key starts from 0 so the keys will be duplicated. The putAll() does not work as it overrides the keys and always gives me the last map.
I have seen examples of merging two maps by using Stream but in my case, there might be more than 2 maps. I am trying to generate a merged map that has an incremental key. For example:
Let's say I have 2 maps in the list (could be more) and both keys start from 0 but ends at different values.
1st map, the key starts at 0 ends at 10
2nd map, the key starts at 0 ends at 15
Is it possible to add the second map with the key starting at 11?
In the end, I need a merged map in which the first key starts at 0 and the last key ends at 25.
Assuming you have a list of maps, where the keys of each map are integers in range [0-k], [0-n], [0, r] ... and your resulting map should a key set in the range of [0 - (k+n+r..)] something like below should work:
public static void main(String[] args) throws IOException {
//example list of maps
List<Map<Integer,List<String>>> mapList = List.of(
Map.of( 0,List.of("foo","foo"),
1,List.of("bar","bar"),
2,List.of("baz","baz")),
Map.of( 0,List.of("doo","doo"),
1,List.of("gee","gee"),
2,List.of("woo","woo")),
Map.of( 0,List.of("cab","cab"),
1,List.of("kii","kii"),
2,List.of("taa","taa"))
);
AtomicInteger ai = new AtomicInteger();
Map<Integer,List<String>> result =
mapList.stream()
.flatMap(map -> map.values().stream())
.collect(Collectors.toMap(list -> ai.getAndIncrement(), Function.identity()));
result.forEach((k,v) ->{
System.out.println(k + " : " + v);
});
}
I'd iterate over any number of maps you have, and then for each map you want to combine, iterate over the entries. For each entry you can use computeIfAbsent to conditionally create an empty list for the key, and then call addAll on the value. E.g.:
List<Map<Integer, List<String>>> maps = List.of(
Map.of(1, List.of("hello")),
Map.of(2, List.of("world")),
Map.of(1, List.of("a"), 2, List.of("b"))
);
Map<Integer, List<String>> combined = new HashMap<>();
for (Map<Integer, List<String>> map : maps) {
for (Map.Entry<Integer, List<String>> e : map.entrySet()) {
combined.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).addAll(e.getValue());
}
}
If you prefer a streams approach
Map<Integer, List<String>> m1;
Map<Integer, List<String>> m2;
Map<Integer, List<String>> m3;
Map<Integer, List<String>> combined = new HashMap<>();
Stream
.of(m1, m2, m3)
.flatMap(m -> m.entrySet().stream())
.forEach(e -> combined.computeIfAbsent(e.getKey(), k -> new ArrayList<>())
.addAll(e.getValue()));
I know you asked for a map, but based on your explanation your ultimate solution and the fact that your keys are now sequential integers starting at 0, you could create just a List<List<String>>. In that case, you could do it like this:
List<List<String>> result = mapList.stream()
.flatMap(map->map.values().stream())
.collect(Collectors.toList());

Summing values of an object by first filtering other unique values of the same object

Let's say I have a list of an object lets call this object Order which has a quantity and price as fields.
for example as below:
Object Order (fields quantity and price) list contains the below values:
Quantity Price
5 200
6 100
3 200
1 300
Now I want to use Java-8 to fetch this list filtered in below way:
Quantity Price
8 200
6 100
1 300
Price being the unique value to filter from and summing any quantity that the price has, I want to form a new list based on this.
Please suggest how i can do this with java 8 lambda expression, thanks.
The following Stream does the trick:
List<Order> o = orders.stream().collect(
Collectors.collectingAndThen(
Collectors.groupingBy(Order::getPrice,Collectors.summingInt(Order::getQuantity)),
map -> map.entrySet().stream()
.map(e -> new Order(e.getKey(), e.getValue()))
.collect(Collectors.toList())));
Let's break this down:
The following code returns a Map<Integer, Integer> which contains price as a key (the unique value you want to base the summing on) and its summed quantities. The key method is Collectors.groupingBy with classifier describing the key and the downstream defining a value, which would be a sum of quantities, hence Collectors.summingInt (depends on the quantity type):
Map<Integer, Integer> map = orders.stream().collect(
Collectors.groupingBy( // I want a Map<Integer, Integer>
Order::getPrice, // price is the key
Collectors.summingInt(Order::getQuantity)) // sum of quantities is the value
The desired structure is List<Order>, therefore you want to use the Collectors.collectingAndThen method with a Collector<T, A, R> downstream and Function<R, RR> finisher. The downstream is the grouping from the first point, the finisher will be a conversion of Map<Integer, Integer> back to List<Order>:
List<Order> o = orders.stream().collect(
Collectors.collectingAndThen(
grouping, // you know this one ;)
map -> map.entrySet()
.stream() // iterate entries
.map(e -> new Order(e.getKey(), e.getValue())) // new Order(qty, price)
.collect(Collectors.toList()))); // as a List<Order>

How to get a custom type instead of Integer when using Collectors.summingInt?

I am currently creating a Map<String, Map<LocalDate, Integer>> like this, where the Integer represents seconds:
Map<String, Map<LocalDate, Integer>> map = stream.collect(Collectors.groupingBy(
x -> x.getProject(),
Collectors.groupingBy(
x -> x.getDate(),
Collectors.summingInt(t -> t.getDuration().toSecondOfDay())
)
));
How could I instead create a Map<String, Map<LocalDate, Duration>>?
To change that Integer from Collectors.summingInt to a Duration, you simply need to replace that Collector with:
Collectors.collectingAndThen(
Collectors.summingInt(t -> t.getDuration().toSecondOfDay()),
Duration::ofSeconds
)
If you were using an actual Duration for getDuration() (instead of LocalTime), you could also sum directly the Duration's as follows:
Map<String, Map<LocalDate, Duration>> map = stream.collect(Collectors.groupingBy(
MyObject::getProject,
Collectors.groupingBy(
MyObject::getDate,
Collectors.mapping(MyObject::getDuration,
Collectors.reducing(Duration.ZERO, Duration::plus))
)
));
With the advantage that it also sums the nanoseconds, and could be generalized to other types as well.
Note however that it creates many intermediate Duration instances which could have an impact on the performance.

Filter TreeMap of dates by maximum value of each month

I have a collection like below currently stored as a TreeMap for sorting. Note that each month has multiple entries. How can I use Java 8 streams to filter this by the max value in each month?
date=value
2010-01-01=2100.00,
2010-01-02=2108.74,
2010-02-01=2208.74,
2010-02-02=2217.92,
2010-03-01=2317.92,
2010-03-02=2327.57,
2010-04-01=2427.57,
2010-04-02=2437.67,
2010-05-01=2537.67,
2010-05-02=2548.22,
2010-06-01=2648.22,
2010-06-02=2659.24,
2010-07-01=2759.24,
2010-07-02=2770.72,
2010-08-01=2870.72,
2010-08-02=2882.66,
2010-09-01=2982.66,
2010-09-02=2995.07,
2010-10-01=3095.07,
2010-10-02=3107.94,
2010-11-01=3207.94,
2010-11-02=3221.29
A possible solution is the following:
create a Stream over all the entries in the Map
collect that Stream into a new Map where the key corresponds to the year-month part of the Map and the value is the current entry. In case of duplicates, only the maximum element with regard to the date will be kept
create a new Stream again on the values of that intermediate Map
and finally collect it into a TreeMap.
Assuming the initial Map is of type TreeMap<LocalDate, Double>, this would be an implementation (this code uses static imports from the Collectors class):
TreeMap<LocalDate, Double> filtered =
map.entrySet()
.stream()
.collect(groupingBy(
e -> YearMonth.from(e.getKey()),
collectingAndThen(maxBy(Map.Entry.comparingByKey()), Optional::get))
)
.values()
.stream()
.collect(toMap(
Map.Entry::getKey,
Map.Entry::getValue,
(v1, v2) -> { throw new IllegalStateException(); },
TreeMap::new)
);
In this code, the map is first grouped by the year-month using Collectors.groupingBy(classifier, downstream). The classifier returns a YearMonth object from the LocalDate. The downstream collector is used to collect all the values having the same year-month into a single value: in this case, we therefore use Collectors.maxBy(comparator) to select the maximum value according to the comparator comparing each entry LocalDate key (comparingByKey). Since this collector returns an Optional (in case the Stream is empty), we wrap it into a Collectors.collectingAndThen(downstream, finisher) where the finisher just returns the optional value. At the end of this step, we therefore have a Map<YearMonth, Map.Entry<LocalDate, Double>>.
Finally, we keep the values of this intermediate map to collect each entry into a new Map, where we explicitly create a TreeMap. Since we know there are no duplicates here, the merging function simply throws a IllegalStateException.
Sample input / output :
2010-01-01=2100.00
2010-01-02=2108.74
2010-02-01=2208.74
2010-02-02=2217.92
2010-03-01=2317.92
2010-03-02=2327.57
2010-04-01=2427.57
->
2010-01-02=2108.74
2010-02-02=2217.92
2010-03-02=2327.57
2010-04-01=2427.57
I'm assuming you're using LocalDate for this:
TreeMap<LocalDate, Double> map = new TreeMap<>();
// ...
Here's a functional solution
Map<Month, Optional<Double>> max =
map.entrySet()
.stream()
.collect(Collectors.groupingBy(
e -> e.getKey().getMonth(),
TreeMap::new,
Collectors.mapping(
e -> e.getValue(),
Collectors.maxBy(Comparator.naturalOrder())
)));
System.out.println(max);
Which prints
{JANUARY=Optional[2108.74], FEBRUARY=Optional[2217.92], MARCH=Optional[2327.57], ...}
Here's an imperative solution (just in case)
Map<Month, Double> max = new TreeMap<>();
for (Entry<LocalDate, Double> entry : map.entrySet()) {
Month month = entry.getKey().getMonth();
Double value = max.get(month);
if (value == null || value < entry.getValue())
max.put(month, entry.getValue());
}
System.out.println(max);
Which prints
{JANUARY=2108.74, FEBRUARY=2217.92, MARCH=2327.57, ...}

Categories