Calculate sum of list within a Map of Map - java

I have a list of Trades that is a map of map grouped by Region and Status. Trade class has an attribute amount field.
Map<Region, Map<Status, List<Trade>>> groupedTrades
class Trade {
double amount;
}
I want to group the amounts across Trades within the list and return it as below
Map<Region, Map<Status, Double>> sumOfGroupedTradeAmounts
Double is a sum of all the amount fields within the list of trades.
How can I do this is in java8?

You can do it like this. Basically, you are:
Doing nested streaming of the entrySets.
Stream the outer map entries to get the inner map values and the outer map key.
in the inner entry set, stream the value, which is the List<Doubles> and sum them.
these are then returned in the specified map using the outer map key, inner map key and a sum of the Trade amounts resulting in a double value.
Note that I added a getter to the Trade class for amount retrieval.
class Trade {
double amount;
public double getAmount() {
return amount;
}
}
public static void main(String[] args) {
Map<Region, Map<Status, Double>> result = groupedTrades
.entrySet().stream()
// outer map starts here, keying on Region
.collect(Collectors.toMap(Entry::getKey, e -> e
.getValue().entrySet().stream()
// inner map starts here, keying on Status
.collect(Collectors.toMap(Entry::getKey,
// stream the list and sum the amounts.
ee -> ee.getValue().stream()
.mapToDouble(Trade::getAmount)
.sum()))));
}
Given the following structure where Region and Status are numbers and letters respectively,
Map<Region, Map<Status, List<Trade>>> groupedTrades = Map.of(
new Region(1),
Map.of(new Status("A"),
List.of(new Trade(10), new Trade(20),
new Trade(30)),
new Status("B"),
List.of(new Trade(1), new Trade(2))),
new Region(2),
Map.of(new Status("A"),
List.of(new Trade(2), new Trade(4),
new Trade(6)),
new Status("B"), List.of(new Trade(3),
new Trade(6), new Trade(9))));
result.forEach((k, v) -> {
System.out.println(k);
v.forEach((kk, vv) -> System.out
.println(" " + kk + " -> " + vv));
});
Here is the sample output.
2
B -> 18.0
A -> 12.0
1
B -> 3.0
A -> 60.0
If you're interested in suming all of the values. you can do it as follows by using the just created nested map of doubles.
double allSums =
// Stream the inner maps
result.values().stream()
// put all the Doubles in a single stream
.flatMap(m->m.values().stream())
// unbox the Doubles to primitive doubles
.mapToDouble(Double::doubleValue)
// and sum them
.sum();

Using only streams it might look like as follows:
Map<Region, Map<Status, Double>> result =
groupedTrades
.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey, // Leave key as is
entry -> mapToDouble(entry.getValue(), MyClass::sumOfTrades))); // applies sumOfTrades (See below)
And we define little helper function, in order to keep our stream readable:
// Takes map which contains lists of trades and and applies function mapper, which returns T (might be double) on those lists
private static <T> Map<Status, T> mapToDouble(Map<Status, List<Trade>> trades, Function<List<Trade>, T> mapper) {
return trades.entrySet()
.stream()
.collect(Collectors.toMap(
Map.Entry::getKey, // Leave key as is
entry -> mapper.apply(entry.getValue())));
}
// Takes a list of trades and returns its sum
private static double sumOfTrades(List<Trade> trades) {
return trades.stream().mapToDouble(Trade::getAmount).sum();
}
Bonus - sum of all trades:
double sum =
groupedTrades
// get collection of "inner" maps
.values()
.stream()
// get only their values as collection of lists
.map(Map::values)
// map the collection into stream of lists
.flatMap(Collection::stream)
// map the collection stream of lists into stream of Trade values
.flatMap(Collection::stream)
// map Trade into double value - "amount"
.mapToDouble(Trade::getAmount)
// get the sum
.sum();

Related

Have Java Streams GroupingBy result Map include a key for each value of an enum, even if value is an empty List

This question is about Java Streams' groupingBy capability.
Suppose I have a class, WorldCup:
public class WorldCup {
int year;
Country champion;
// all-arg constructor, getter/setters, etc
}
and an enum, Country:
public enum Country {
Brazil, France, USA
}
and the following code snippet:
WorldCup wc94 = new WorldCup(1994, Country.Brazil);
WorldCup wc98 = new WorldCup(1998, Country.France);
List<WorldCup> wcList = new ArrayList<WorldCup>();
wcList.add(wc94);
wcList.add(wc98);
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.groupingBy(WorldCup::getCountry, Collectors.mapping(WorldCup::getYear));
After running this code, championsMap will contain:
Brazil: [1994]
France: [1998]
Is there a succinct way to have this list include an entry for all of the values of the enum? What I'm looking for is:
Brazil: [1994]
France: [1998]
USA: []
There are several approaches you can take.
The map which would be used for accumulating the stream data can be prepopulated with entries corresponding to every enum-member. To access all existing enum-members you can use values() method or EnumSet.allOf().
It can be achieved using three-args version of collect() or through a custom collector created via Collector.of().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(
() -> EnumSet.allOf(Country.class).stream() // supplier
.collect(Collectors.toMap(
Function.identity(),
c -> new ArrayList<>()
)),
(Map<Country, List<Integer>> map, WorldCup next) -> // accumulator
map.get(next.getCountry()).add(next.getYear()),
(left, right) -> // combiner
right.forEach((k, v) -> left.get(k).addAll(v))
);
Another option is to add missing entries to the map after reduction of the stream has been finished.
For that we can use built-in collector collectingAndThen().
Map<Country, List<Integer>> championsMap = wcList.stream()
.collect(Collectors.collectingAndThen(
Collectors.groupingBy(WorldCup::getCountry,
Collectors.mapping(WorldCup::getYear,
Collectors.toList())),
map -> {
EnumSet.allOf(Country.class)
.forEach(country -> map.computeIfAbsent(country, k -> new ArrayList<>())); // if you're not going to mutate these lists - use Collections.emptyList()
return map;
}
));

How to put the string stored in the treeMap in java

I have a String a = "1x^2 2x^3 3x^4 4x^4 " and I want to store the power in each term as key and coefficient as value to treeMap. Since there is two same power with different coefficients, I need to sum up coefficients so that the result is {2=1,3=2,4=7}. Because treemap.put(key) can only override the previous value with the same key so I could only get the result as{2=1,3=2,4=4}. How to solve that?
It is recommended to use Map::merge function to accumulate the values for multiple identical keys:
Map<Integer, Integer> terms = new TreeMap<>();
for (String term : a.split("\\s+")) {
String[] pair = term.split("x\\^");
terms.merge(Integer.valueOf(pair[1]), Integer.valueOf(pair[0]), Integer::sum);
}
System.out.println(terms);
Output:
{2=1, 3=2, 4=7}
Using Stream API this can be resolved using Collectors.toMap in a similar way using Integer::sum as a merge function:
String a = "1x^2 2x^3 3x^4 4x^4 ";
Map<Integer, Integer> terms = Arrays.stream(a.split("\\s+")) // Stream<String>
.map(t -> t.split("x\\^")) // Stream<String[]>
.collect(Collectors.toMap(
t -> Integer.valueOf(t[1]), // power as key
t -> Integer.valueOf(t[0]), // coefficient as value
Integer::sum, // add coefficients for the same power
TreeMap::new // supply TreeMap as result
));
System.out.println(terms);
Output:
{2=1, 3=2, 4=7}

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>

Adding two lists of own type

I have a simple User class with a String and an int property.
I would like to add two Lists of users this way:
if the String equals then the numbers should be added and that would be its new value.
The new list should include all users with proper values.
Like this:
List1: { [a:2], [b:3] }
List2: { [b:4], [c:5] }
ResultList: {[a:2], [b:7], [c:5]}
User definition:
public class User {
private String name;
private int comments;
}
My method:
public List<User> addTwoList(List<User> first, List<User> sec) {
List<User> result = new ArrayList<>();
for (int i=0; i<first.size(); i++) {
Boolean bsin = false;
Boolean isin = false;
for (int j=0; j<sec.size(); j++) {
isin = false;
if (first.get(i).getName().equals(sec.get(j).getName())) {
int value= first.get(i).getComments() + sec.get(j).getComments();
result.add(new User(first.get(i).getName(), value));
isin = true;
bsin = true;
}
if (!isin) {result.add(sec.get(j));}
}
if (!bsin) {result.add(first.get(i));}
}
return result;
}
But it adds a whole lot of things to the list.
This is better done via the toMap collector:
Collection<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values();
First, concatenate both the lists into a single Stream<User> via Stream.concat.
Second, we use the toMap collector to merge users that happen to have the same Name and get back a result of Collection<User>.
if you strictly want a List<User> then pass the result into the ArrayList constructor i.e. List<User> resultSet = new ArrayList<>(result);
Kudos to #davidxxx, you could collect to a list directly from the pipeline and avoid an intermediate variable creation with:
List<User> result = Stream
.concat(first.stream(), second.stream())
.collect(Collectors.toMap(
User::getName,
u -> new User(u.getName(), u.getComments()),
(l, r) -> {
l.setComments(l.getComments() + r.getComments());
return l;
}))
.values()
.stream()
.collect(Collectors.toList());
You have to use an intermediate map to merge users from both lists by summing their ages.
One way is with streams, as shown in Aomine's answer. Here's another way, without streams:
Map<String, Integer> map = new LinkedHashMap<>();
list1.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
list2.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Now, you can create a list of users, as follows:
List<User> result = new ArrayList<>();
map.forEach((name, comments) -> result.add(new User(name, comments)));
This assumes User has a constructor that accepts name and comments.
EDIT: As suggested by #davidxxx, we could improve the code by factoring out the first part:
BiConsumer<List<User>, Map<String, Integer>> action = (list, map) ->
list.forEach(u -> map.merge(u.getName(), u.getComments(), Integer::sum));
Map<String, Integer> map = new LinkedHashMap<>();
action.accept(list1, map);
action.accept(list2, map);
This refactor would avoid DRY.
There is a pretty direct way using Collectors.groupingBy and Collectors.reducing which doesnt require setters, which is the biggest advantage since you can keep the User immutable:
Collection<Optional<User>> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
// and reducing the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments()))))
.values(); // return values from Map<String, List<User>>
Unfortunately, the result is Collection<Optional<User>> since the reducing pipeline returns Optional since the result might not be present after all. You can stream the values and use the map() to get rid of the Optional or use Collectors.collectAndThen*:
Collection<User> d = Stream
.of(first, second) // start with Stream<List<User>>
.flatMap(List::stream) // flatting to the Stream<User>
.collect(Collectors.groupingBy( // Collecting to Map<String, List<User>>
User::getName, // by name (the key)
Collectors.collectingAndThen( // reduce the list into a single User
Collectors.reducing((l, r) -> new User(l.getName(), l.getComments() + r.getComments())),
Optional::get))) // and extract from the Optional
.values();
* Thanks to #Aomine
As alternative fairly straight and efficient :
stream the elements
collect them into a Map<String, Integer> to associate each name to the sum of comments (int)
stream the entries of the collected map to create the List of User.
Alternatively for the third step you could apply a finishing transformation to the Map collector with collectingAndThen(groupingBy()..., m -> ...
but I don't find it always very readable and here we could do without.
It would give :
List<User> users =
Stream.concat(first.stream(), second.stream())
.collect(groupingBy(User::getName, summingInt(User::getComments)))
.entrySet()
.stream()
.map(e -> new User(e.getKey(), e.getValue()))
.collect(toList());

Java 8 stream Map<String, List<String>> sum of values for each key

I am not so familiar with Java 8 (still learning) and looking to see if I could find something equivalent of the below code using streams.
The below code mainly tries to get corresponding double value for each value in String and then sums it up. I could not find much help anywhere on this format. I am not sure if using streams would clean up the code or would make it messier.
// safe assumptions - String/List (Key/Value) cannot be null or empty
// inputMap --> Map<String, List<String>>
Map<String, Double> finalResult = new HashMap<>();
for (Map.Entry<String, List<String>> entry : inputMap.entrySet()) {
Double score = 0.0;
for (String current: entry.getValue()) {
score += computeScore(current);
}
finalResult.put(entry.getKey(), score);
}
private Double computeScore(String a) { .. }
Map<String, Double> finalResult = inputMap.entrySet()
.stream()
.collect(Collectors.toMap(
Entry::getKey,
e -> e.getValue()
.stream()
.mapToDouble(str -> computeScore(str))
.sum()));
Above code iterates over the map and creates a new map with same keys & before putting the values, it first iterates over each value - which is a list, computes score via calling computeScore() over each list element and then sums the scores collected to be put in the value.
You could also use the forEach method along with the stream API to yield the result you're seeking.
Map<String, Double> resultSet = new HashMap<>();
inputMap.forEach((k, v) -> resultSet.put(k, v.stream()
.mapToDouble(s -> computeScore(s)).sum()));
s -> computeScore(s) could be changed to use a method reference i.e. T::computeScore where T is the name of the class containing computeScore.
How about this one:
Map<String, Double> finalResult = inputMap.entrySet()
.stream()
.map(entry -> new AbstractMap.SimpleEntry<String, Double>( // maps each key to a new
// Entry<String, Double>
entry.getKey(), // the same key
entry.getValue().stream()
.mapToDouble(string -> computeScore(string)).sum())) // List<String> mapped to
// List<Double> and summed
.collect(Collectors.toMap(Entry::getKey, Entry::getValue)); // collected by the same
// key and a newly
// calulcated value
The version above could be merged to the single collect(..) method:
Map<String, Double> finalResult = inputMap.entrySet()
.stream()
.collect(Collectors.toMap(
Entry::getKey, // keeps the same key
entry -> entry.getValue()
.stream() // List<String> -> Stream<String>
// then Stream<String> -> Stream<Double>
.mapToDouble(string -> computeScore(string))
.sum())); // and summed
The key parts:
collect(..) performs a reduction on the elements using a certain strategy with a Collector.
Entry::getKey is a shortcut for entry -> entry.getKey. A function for mapping the key.
entry -> entry.getValue().stream() returns the Stream<String>
mapToDouble(..) returns the DoubleStream. This has an aggregating operation sum(..) which sums the elements - together creates a new value for the Map.
Regardless of whether you use the stream-based or the loop-based solution, it would be beneficial and add some clarity and structure to extract the inner loop into a method:
private double computeScore(Collection<String> strings)
{
return strings.stream().mapToDouble(this::computeScore).sum();
}
Of course, this could also be implemented using a loop, but ... that's exactly the point: This method can now be called, either in the outer loop, or on the values of a stream of map entries.
The outer loop or stream could also be pulled into a method. In the example below, I generalized this a bit: The type of the keys of the map does not matter. Neither does whether the values are List or Collection instances.
As an alternative to the currently accepted answer, the stream-based solution here does not fill a new map that is created manually. Instead, it uses a Collector.
(This is similar to other answers, but I think that the extracted computeScore method greatly simplifies the otherwise rather ugly lambdas that are necessary for the nested streams)
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.stream.Collectors;
public class ToStreamOrNotToStream
{
public static void main(String[] args)
{
ToStreamOrNotToStream t = new ToStreamOrNotToStream();
Map<String, List<String>> inputMap =
new LinkedHashMap<String, List<String>>();
inputMap.put("A", Arrays.asList("1.0", "2.0", "3.0"));
inputMap.put("B", Arrays.asList("2.0", "3.0", "4.0"));
inputMap.put("C", Arrays.asList("3.0", "4.0", "5.0"));
System.out.println("Result A: " + t.computeA(inputMap));
System.out.println("Result B: " + t.computeB(inputMap));
}
private <T> Map<T, Double> computeA(
Map<T, ? extends Collection<String>> inputMap)
{
Map<T, Double> finalResult = new HashMap<>();
for (Entry<T, ? extends Collection<String>> entry : inputMap.entrySet())
{
double score = computeScore(entry.getValue());
finalResult.put(entry.getKey(), score);
}
return finalResult;
}
private <T> Map<T, Double> computeB(
Map<T, ? extends Collection<String>> inputMap)
{
return inputMap.entrySet().stream().collect(
Collectors.toMap(Entry::getKey, e -> computeScore(e.getValue())));
}
private double computeScore(Collection<String> strings)
{
return strings.stream().mapToDouble(this::computeScore).sum();
}
private double computeScore(String a)
{
return Double.parseDouble(a);
}
}
I found it somewhat shorter:
value = startDates.entrySet().stream().mapToDouble(Entry::getValue).sum();

Categories