4

I have a object list, which I need to group by 2 different atributes and then sum the values of an attribute, the structure of my object is something like this:

private Long id1;
private Long id2;
private Double amountReserved;
private Double amountRequired;
//... more atributes and getters and setters

So, I then have a list, for example:

List<MyList> list = Arrays.asList(
list(1, A, 50, 200)
list(1, A, 50, 200)
list(1, B, 0, 100)
list(2, A, 10, 15)
list(2, A, 5, 15)
list(3, A, 0, 25));

What I am trying to achieve is a new list with the below structure:

list(1, A, 100, 100) 
list(1, B,   0, 100)
list(2, A,  15,   0)
list(3, A,   0,  25)

Elucidating what is the requisite I am trying to achieve:

  1. Group objects by id1 and id2
  2. sum the amountReservedof the grouped object
  3. subtract amountRequired from the summed amountReserved

What I have so far:

This one got me the groupings as I wanted

Map<Long, Map<String, List<MyList>>> map = null;
map = lista.stream().collect(Collectors.groupingBy(PreSeparacaoDto::getCodigoPedido,
                    Collectors.groupingBy(PreSeparacaoDto::getCodigoProduto)));

This one sums by group id1, but I am struggling to add the second groupingby on it, as I get syntax errors:

lista.stream()
        .collect(Collectors.groupingBy(PreSeparacaoDto::getCodigoPedido,
                Collectors.summingDouble(PreSeparacaoDto::getProdutoQuantidadeSeparada)))
        .forEach((codigoPedido, ProdutoQuantidadeSeparada) -> System.out.println( codigoPedido + ": " + ProdutoQuantidadeSeparada  ));

My problem is that I failed to get those together ( as per requisite 2) and was not even close to achieve my requisite 3.

I tried to use reduction, as explained here , but honestly, I was not able to replicate it with a single grouping, the reducing is returning an error informing that my parameters don't meet the reducing parameters. I looked for some other options here on stackoverflow and other websites, but without success.

Can someone help me out and poiting where I am failing to combine the reduction with my group, or if that is the correct path I should be following.

2
  • Is there a certainty regarding subtract amountRequired from the summed amountReserved, such that amountRequired would always be same for such common entries ? For e.g. list(2, A, 10, 15) list(2, A, 5, 15) , I think you've deducted 15 from 15 and the output is list(2, A, 15, 0)...But what if the input was list(2, A, 10, 15) list(2, A, 5, 30) ? Commented Mar 1, 2019 at 15:10
  • Good point, forgot to make clear that the amountRequired is shared among all list itens from the same id1 and id2. Commented Mar 1, 2019 at 16:10

4 Answers 4

2

I think an easy way is to use Collectors.grouping : you tell it how to group and what to collect.

Here's an example, computing only the sum of AmountReserved :

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class GroupedSums {

    static class MyList {
        Long id1;
        char id2;
        Double amountReserved;
        Double amountRequired;
        public Long getId1() {
            return id1;
        }
        public char getId2() {
            return id2;
        }
        public Double getAmountReserved() {
            return amountReserved;
        }
        public Double getAmountRequired() {
            return amountRequired;
        }
        public MyList(Long id1, char id2, Double amountReserved, Double amountRequired) {
            super();
            this.id1 = id1;
            this.id2 = id2;
            this.amountReserved = amountReserved;
            this.amountRequired = amountRequired;
        }

        Key key() {
            return new Key(id1, id2);
        }

    }

    private static MyList list(Long id1, char id2, Double amountReserved, Double amountRequired) {
        return new MyList(id1, id2, amountReserved, amountRequired);
    }

    public GroupedSums() {      
    }

    private static class Key {

        Long id1;
        char id2;
        public Long getId1() {
            return id1;
        }
        public char getId2() {
            return id2;
        }
        public Key(Long id1, char id2) {
            super();
            this.id1 = id1;
            this.id2 = id2;
        }
        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = prime * result + ((id1 == null) ? 0 : id1.hashCode());
            result = prime * result + id2;
            return result;
        }
        @Override
        public boolean equals(Object obj) {
            if (this == obj)
                return true;
            if (obj == null)
                return false;
            if (getClass() != obj.getClass())
                return false;
            Key other = (Key) obj;
            if (id1 == null) {
                if (other.id1 != null)
                    return false;
            } else if (!id1.equals(other.id1))
                return false;
            if (id2 != other.id2)
                return false;
            return true;
        }
        @Override
        public String toString() {
            return "[id1=" + id1 + ", id2=" + id2 + "]";
        }   

    }

    public static void main(String[] args) {
        List<MyList> list = Arrays.asList(
                list(1L, 'A', 50d, 200d),
                list(1L, 'A', 50d, 200d),
                list(1L, 'B', 0d, 100d),
                list(2L, 'A', 10d, 15d),
                list(2L, 'A', 5d, 15d),
                list(3L, 'A', 0d, 25d));

        list.stream().collect(Collectors.groupingBy(MyList::key, Collectors.summingDouble(MyList::getAmountReserved)))
        .forEach((k,v)->System.out.println("" + k + " :" + v));
    }

}

HTH!

Sign up to request clarification or add additional context in comments.

Comments

1

You might just be looking for simply Collectors.toMap as :

List<MyList> output = new ArrayList<>(lista.stream()
        .collect(Collectors.toMap(a -> a.getId1() + "-" + a.getId2(), a -> a, (myList1, myList2) -> {
            myList1.amountReserved = myList1.amountReserved + myList2.amountReserved;
            myList1.amountRequired = myList1.amountRequired - myList1.amountReserved;
            return myList1;
        })).values());

5 Comments

Note: You can update the mergeFunction based on the answer to this comment as well. This is more towards the approach of choosing a better key in the map and then possibly finding the list of values.
I wouldn't concatenate two integer types, as the resulting string might collide with another concatenation of two integers, i.e. 2 + 35 equals 23 + 5.
@MCEmperor Well, the idea in mind was to actually create a function out of it. (for both the first and the second id). Though agreeing with the fact that the implementation I'd shared here is flawed in cases like you've shared.
OK, so you concatenate id1 and id2, so there is no need to create a 'group of a group', and by using "-" you avoid the case mentioned by Emperor. I just got lost on a -> a, (myList1, myList2) ->. I don't really understand what is happening on that arrow function, could you point me to some information so I can understand what's happening in there? I confess that I'm having a hard time figuring out exactly what I need to google for...
Just a note, the amountRequired is not being calculated correctly inside the function. For some reason, when there are groupings to be done, the calculation does not work, so I had run that part of the code outside the stream, iterating through the list again using a simple foreach. As the rest, so far your code solved my problem.Thanks.
1

You can stream over the input list twice.

First time, you group by id1, id2 and compute the sum of amount reserved. Second time, you can stream the list again, group it (by id1 and id2) by making use of the above result to find the difference.

Map<Long, Map<Long, Double>> amountReservedGroup = list.stream()
        .collect(Collectors.groupingBy(MyList::getId1, Collectors.groupingBy(MyList::getId2,
                Collectors.summingDouble(MyList::getAmountReserved))));


Map<Long, Map<Long, List<MyList>>> finalResult = list.stream()
        .collect(Collectors.groupingBy(MyList::getId1, Collectors.groupingBy(MyList::getId2,
                Collectors.mapping(o -> new MyList(o.getId1(), o.getId2(),
                                amountReservedGroup.get(o.getId1()).get(o.getId2()),
                                o.getAmountRequired() - amountReservedGroup.get(o.getId1()).get(o.getId2())),
                        Collectors.toList()))));

Note: This does not handle the case when the result of the subtraction is negative!!

As pointed out by nullpointer@ in the comments, will the value of amountRequired be the same for a given id1 and id2?

Comments

0

you can do order by id1 and then order id2 (to make sure elements of the same list and sublist are after each other) and then you do nested foreach (before you iterate the sublist, you init result_reserved_amount to 0 and result_required_amount to the initial value) then you do if same ids (if id1= previous_id1 and id2 = previous_id2) do result_reserved_amount+= current_reserved_amount and result_required_amount -= current_reserved_amount, otherwise update previous_id1, previous_id2, result_reserved_amount, result_required_amount

1 Comment

Thanks for trying to help. It is just that the question requires a solution using java lambda and stream. While your suggestion is likely to work, it does not meet the requirements of an acceptable answer. Nonetheless, thank you for the time writting up a possible solution.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.