Search code examples
javajava-8java-streamgroupingby

Java 8 streams group by 3 fields and aggregate other two fileds by sum


I am new to Java 8, and have had trouble implementing already provided solution here on a similar kind of issue. Please help.

In Java 8 group by how to group by on three fields which returns more than one row that has to sum up on the rest of two Integer fields. Here in the below dto/pojo class need to make a sum up of incomingCount and outgoingCount fields based on the unique key of uuid, msgDate, and channel combined.

public class ReportData {

    private String uuid;
    private String msgDate;
    private String channel;
    private Integer incomingCount;
    private Integer outgoingCount;
}

//Initializing List as sample.

List<ReportData> list1 = new ArrayList<>();

list1.add(new ReportData("c9c3a519","December 2023", "digital", 5, 0 ));
list1.add(new ReportData("c9c3a519","December 2023", "digital", 3, 0 ));
list1.add(new ReportData("c9c3a519","December 2023", "digital", 0, 3 ));
list1.add(new ReportData("c9c3a519","November 2023", "digital", 4, 0 ));
list1.add(new ReportData("c9c3a519","November 2023", "digital", 0, 4 ));
list1.add(new ReportData("c9c3a519","December 2023", "manual", 5, 0 ));
list1.add(new ReportData("c9c3a519","December 2023", "manual", 3, 0 ));
list1.add(new ReportData("c9c3a519","December 2023", "manual", 0, 3 ));
list1.add(new ReportData("c9c3a519","November 2023", "manual", 4, 0 ));
list1.add(new ReportData("c9c3a519","November 2023", "manual", 0, 4 ));
list1.add(new ReportData("3de4c44f","December 2023", "digital", 5, 0 ));
list1.add(new ReportData("3de4c44f","December 2023", "digital", 0, 3 ));
list1.add(new ReportData("3de4c44f","November 2023", "digital", 4, 0 ));
list1.add(new ReportData("3de4c44f","November 2023", "digital", 0, 4 ));
list1.add(new ReportData("3de4c44f","December 2023", "manual", 5, 0 ));
list1.add(new ReportData("3de4c44f","December 2023", "manual", 0, 3 ));
list1.add(new ReportData("3de4c44f","November 2023", "manual", 4, 0 ));
list1.add(new ReportData("3de4c44f","November 2023", "manual", 0, 4 ));

Output Object should have data as below:

uuid msgDate channel incomingCount outgoingCount

c9c3a519 December 2023 digital 8 3
c9c3a519 November 2023 digital 4 4
c9c3a519 December 2023 manual 8 3
c9c3a519 November 2023 manual 4 4
...
...
...


Solution

  • Collect the result into a Map. This example will use Collectors.toMap(keyMapper, valueMapper, mergeFunction, mapFactory).

    Returns a Collector that accumulates elements into a Map whose keys and values are the result of applying the provided mapping functions to the input elements. If the mapped keys contains duplicates (according to Object.equals(Object)), the value mapping function is applied to each equal element, and the results are merged using the provided merging function. The Map is created by a provided supplier function.

    Also I am using lombok annotations for brevity.

    First start by creating classes to represent the keys you want to group by and the aggregated data:

    @AllArgsConstructor
    @Getter
    public class Count {
    
      private final int in;
      private final int out;
    
      public Count merge(Count other) {
        return new Count(this.in + other.in, this.out + other.out);
      }
    
      @Override
      public String toString() {
        return in + " " + out;
      }
    }
    
    @AllArgsConstructor
    public class Key {
    
      private final String uuid;
      private final String date;
      private final String channel;
    
      @Override
      public int hashCode() {
        return Objects.hash(uuid, date, channel);
      }
    
      @Override
      public boolean equals(Object obj) {
        if (this == obj) {
          return true;
        }
        if (!(obj instanceof Key)) {
          return false;
        }
        Key other = (Key) obj;
        return uuid.equals(other.uuid) && date.equals(other.date) && channel.equals(other.channel);
      }
    
      @Override
      public String toString() {
        return uuid + " " + date + " " + channel;
      }
    }
    

    Then extend ReportData with 2 more methods to create the key and initial aggregation:

    @AllArgsConstructor
    public class ReportData {
    
      //the fields
    
      public Key createKey() {
        return new Key(uuid, msgDate, channel);
      }
    
      public Count createCount() {
        return new Count(incomingCount, outgoingCount);
      }
    }
    

    And collect the data:

    public class SoMain {
    
      public static void main(String[] args) {
        List<ReportData> list = new ArrayList<>();
    
        //populate the list
    
        Map<Key, Count> result = list.stream()
                .collect(Collectors.toMap(ReportData::createKey, ReportData::createCount, Count::merge, LinkedHashMap::new));
        for (Map.Entry<Key, Count> entry : result.entrySet()) {
          System.out.println(entry.getKey() + " " + entry.getValue());
        }
      }
    }
    

    The arguments of the Collector are as follows:

    1. ReportData::createKey - creates the key to group by (key of the map)
    2. ReportData::createCount - creates the initial aggregation from a single ReportData (value of the map)
    3. Count::merge - merges two Counts on key collision (see the merge method)
    4. LinkedHashMap::new - factory for a Map to insert the results in. I want to preserve insertion order, but if you don't need to, you can just omit the parameter to use the default factory.

    Prints:

    c9c3a519 December 2023 digital 8 3
    c9c3a519 November 2023 digital 4 4
    c9c3a519 December 2023 manual 8 3
    c9c3a519 November 2023 manual 4 4
    3de4c44f December 2023 digital 5 3
    3de4c44f November 2023 digital 4 4
    3de4c44f December 2023 manual 5 3
    3de4c44f November 2023 manual 4 4