Java stream groupingBy and sum multiple fields
Asked Answered
M

2

2

Here is my List fooList

class Foo {
    private String name;
    private int code;
    private int account;
    private int time;
    private String others;

    ... constructor, getters & setters
}

e.g.(all the value of account has been set to 1)

new Foo(First, 200, 1, 400, other1), 
new Foo(First, 200, 1, 300, other1),
new Foo(First, 201, 1, 10, other1),
new Foo(Second, 400, 1, 20, other2),
new Foo(Second, 400, 1, 40, other2),
new Foo(Third, 100, 1, 200, other3),
new Foo(Third, 101, 1, 900, other3)

I want to transform these values by grouping "name" and "code", accounting for the number, and summing the "time", e.g.

new Foo(First, 200, 2, 700, other1), 
new Foo(First, 201, 1, 10, other1),
new Foo(Second, 400, 2, 60, other2),
new Foo(Third, 100, 1, 200, other3),
new Foo(Third, 101, 1, 900, other3)

I know that I should use a stream like this:

Map<String, List<Foo>> map = fooList.stream().collect(groupingBy(Foo::getName()));

but how can I group them by code then do the accounting and summing job?


Also, what if I want to calculate the average time? e.g.

new Foo(First, 200, 2, 350, other1), 
new Foo(First, 201, 1, 10, other1),
new Foo(Second, 400, 2, 30, other2),
new Foo(Third, 100, 1, 200, other3),
new Foo(Third, 101, 1, 900, other3)

Can I use both of summingInt(Foo::getAccount) and averagingInt(Foo::getTime) instead?

Mcnamara answered 9/6, 2020 at 15:30 Comment(2)
Why your name is String when you pass Integer values?Eigenfunction
maybe I should rename it in a better way@EigenfunctionMcnamara
H
4

A workaround could be to deal with grouping with key as List and casting while mapping back to object type.

List<Foo> result = fooList.stream()
        .collect(Collectors.groupingBy(foo ->
                        Arrays.asList(foo.getName(), foo.getCode(), foo.getAccount()),
                Collectors.summingInt(Foo::getTime)))
        .entrySet().stream()
        .map(entry -> new Foo((String) entry.getKey().get(0),
                (Integer) entry.getKey().get(1),
                entry.getValue(),
                (Integer) entry.getKey().get(2)))
        .collect(Collectors.toList());

Cleaner way would be to expose APIs for merge function and performing a toMap.


Edit: The simplification with toMap would look like the following

List<Foo> result = new ArrayList<>(fooList.stream()
        .collect(Collectors.toMap(foo -> Arrays.asList(foo.getName(), foo.getCode()),
                Function.identity(), Foo::aggregateTime))
        .values());

where the aggregateTime is a static method within Foo such as this :

static Foo aggregateTime(Foo initial, Foo incoming) {
    return new Foo(incoming.getName(), incoming.getCode(),
            incoming.getAccount(), initial.getTime() + incoming.getTime());
}
Holloman answered 9/6, 2020 at 15:43 Comment(7)
It seems that I can't do return new Foo(incoming.getName(),incoming.getCode(),incoming.getAccount(), initial.getTime() + incoming.getTime()); because of required: no argumentsMcnamara
@Mcnamara that just needs an appropriate constructor.Holloman
I'm totally beginner in Java, would you mind teaching me how to do that? Thank you. Also it's kind of confused to me about calculating the average time.@HollomanMcnamara
@Mcnamara What I did there was to iterate through the list and find out elements that are the same based on few attributes. Next, I have defined a merge function aggregateTime which takes two such similar objects and results into a merged final object (based on whatever intended logic). If your expectation is to average the time as well, then you would have to for once iterate the complete list and then evaluated the average of values found matching. But, that wouldn't be the same as summing which doesn't need an awareness of the number of elements traversed.Holloman
I see..So It's better to merge an object which has got the number of groupednameandcode , and summed time, then do a single division to get the average time. Am I right? Also I wanna know what is an appropriate constructor looks like. Thanks a lot! @HollomanMcnamara
@Mcnamara Yes, to get an average, sum with division by size can help. Another way would be to write your custom collector for something similar. By appropriate constructor I meant, that accepts all the arguments as shared in the POJO as in the question. (excluding the others). The intent is to create a new instance with the specified values.Holloman
@Holloman I followed your approach but I couldn't fix my issue. Here is the link : #74512411Cavalry
T
0

You can implement your own collect mechanish that might look like following

        var collect = Stream.of(
                new Foo(1, 200, 1, 400),
                new Foo(1, 200, 1, 300),
                new Foo(1, 201, 1, 10),
                new Foo(2, 400, 1, 20),
                new Foo(2, 400, 1, 40),
                new Foo(3, 100, 1, 200),
                new Foo(3, 101, 1, 900)
        )
                .collect(
                        ArrayList::new,
                        (BiConsumer<ArrayList<Foo>, Foo>) (foos, foo) -> {
                            var newFoo = foos
                                    .stream()
                                    .filter(f -> f.name == foo.name && f.account == foo.account)
                                    .findFirst()
                                    .map(f -> new Foo(f.name, f.code, f.account + foo.account, f.time + foo.time))
                                    .orElse(foo);
                            foos.removeIf(f -> f.name == foo.name && f.account == foo.account);
                            foos.add(newFoo);
                        },
                        (foos, foos2) -> foos.addAll(foos2)
                );
Troche answered 9/6, 2020 at 15:50 Comment(2)
I declared name as int. Anyway that's just a draftTroche
I followed your approach but I couldn't fix my issue. Here is the link : #74512411Cavalry

© 2022 - 2024 — McMap. All rights reserved.