Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Java 8 stream groupingBy: summing an attributeValue

I have a simple class Foo:

class Foo {
   String type; 
   Integer quantity;
   String code;
   //...
}

and a list of objects of that type:

{ type=11, quantity=1, code=A },
{ type=11, quantity=2, code=A },
{ type=11, quantity=1, code=B },
{ type=11, quantity=3, code=C },
{ type=12, quantity=2, code=A },
{ type=12, quantity=1, code=B },
{ type=11, quantity=1, code=B },
{ type=13, quantity=1, code=C },
{ type=13, quantity=3, code=C }

I need to first group by type, and I can do that:

Map<String, List<Foo>> typeGroups = mylist.stream().collect(Collectors.groupingBy(Foo::getType));

The next step would be transforming that Map to a List<Foo> like this:

{ type=11, quantity=3, code=A },
{ type=11, quantity=2, code=B },
{ type=11, quantity=3, code=C },
{ type=12, quantity=2, code=A },
{ type=12, quantity=1, code=B },
{ type=13, quantity=4, code=C }

In SQL, I'd have this kind of query:

SELECT type, code, SUM(quantity) FROM Foo GROUP BY type, code

Let me explain in plain english, too.

I need to end up with a List of Foos. Each one's code must be unique among its group, and quantity must be the aggregate sum of Foos having same code among their group.

I tried doing that using groupingBy() with summingInt() but can't get the desired result.

like image 267
Fabio B. Avatar asked Jan 25 '16 16:01

Fabio B.


2 Answers

You need to use a groupingBy collector that groups according to a list made by the type and the code. This works because two lists are equal when all of their elements are equal and in the same order. (Note that the list here only serves as a container of values with an equality defined as equality of all the values contained and in the same order; a custom Tuple class would also work here).

The following code first creates an intermediate map Map<List<String>, Integer> where the key corresponds to a list of type and code and the value corresponds to the sum of all the quantities for those type and code. That map is then post-processed: each entry are mapped to a Foo where the type is the first value of the key, the quantity is the value and the code is the second value of the key.

Map<List<String>, Integer> map = 
    myList.stream()
          .collect(Collectors.groupingBy(
             f -> Arrays.asList(f.getType(), f.getCode()),
             Collectors.summingInt(Foo::getQuantity)
          ));

List<Foo> result = 
    map.entrySet()
       .stream()
       .map(e -> new Foo(e.getKey().get(0), e.getValue(), e.getKey().get(1)))
       .collect(Collectors.toList());

Notice that the above solution is using 2 different Stream pipelines. This could be done using a single Stream pipeline but it would require a collector that is capable of collecting into 2 different collectors. In this case, the first collector would just keep the first grouped value and the second one would sum the quantities. Unfortunately, there are no such collectors in the Stream API itself. This can be done using the StreamEx library that contains such a collector.

In the following code, the input list is still grouped by a list formed by the type and code of the Foo. What changes is the collector: we're using pairing(c1, c2, finisher) that pairs two collectors and applies the given finisher operation on the result of the collectors.

  • The first collector here is simply first() that returns the first grouped Foo (we don't need the others as we know they will have the same type and code). It is wrapped with a call to collectingAndThen to extract the Optional value out of the first() collector (first() returns an Optional to handle the case where the stream is empty and would collect into an empty Optional; this is not possible here so we can call get() safely).
  • The second collector simply sums all the quantities with summingInt.
  • The finisher operation here combines the Foo returned by the first collector and the result of the summation into a new Foo with the new quantity.

Finally, we only need to keep the values of the resulting map. Since Map.values() returns a Collection, it is wrapped into a ArrayList to have the final result.

List<Foo> result = new ArrayList<>(
    myList.stream()
          .collect(Collectors.groupingBy(
              f -> Arrays.<String>asList(f.getType(), f.getCode()),
              MoreCollectors.pairing(
                 Collectors.collectingAndThen(MoreCollectors.first(), Optional::get),
                 Collectors.summingInt(Foo::getQuantity),
                 (f, s) -> new Foo(f.getType(), s, f.getCode())
              )
          )).values());
like image 154
Tunaki Avatar answered Oct 19 '22 10:10

Tunaki


I think this can be done as follows in one go, assuming

public class Foo {
    private String type;
    private int quantity;
    private String code;

    public Foo(String type, int quantity, String code) {
        this.type = type;
        this.code = code;
        this.quantity = quantity;
    }

    public String getType() {return type;}
    public int getQuantity() {return quantity;}
    public String getCode() {return code;}
}

and List<Foo> foos = new ArrayList<>(); foos.add(new Foo("11", 1, "A")); foos.add(new Foo("11", 2, "A")); foos.add(new Foo("11", 1, "B")); foos.add(new Foo("11", 3, "C")); foos.add(new Foo("12", 2, "A")); foos.add(new Foo("12", 1, "B")); foos.add(new Foo("11", 1, "B")); foos.add(new Foo("13", 1, "C")); foos.add(new Foo("13", 3, "C"));

The following

    foos.stream().collect(
                    toMap(
                            f -> f.getType() + f.getCode(),
                            Function.identity(),
                            (s, a) -> new Foo(
                                        s.getType(), 
                                        s.getQuantity() + a.getQuantity(),
                                        s.getCode()))
                          )
                    .values();        

produces

Foo[type='12'|quantity=2|code='A'] 
Foo[type='13'|quantity=4|code='C'] 
Foo[type='12'|quantity=1|code='B'] 
Foo[type='11'|quantity=3|code='A'] 
Foo[type='11'|quantity=2|code='B'] 
Foo[type='11'|quantity=3|code='C']                       
like image 1
David Soroko Avatar answered Oct 19 '22 09:10

David Soroko