Combining two lists to make a unique list (based on certain field logic)

Viewed 873

I have a specific problem I am trying to solve, and need some assistance with.

I have a POJO with three fields:

  • ID
  • Date
  • Weighted Number

This POJO can be populated from the DB or an external endpoint and will return a list of said POJO. Now we end up with two seperate lists with similar infomation. These lists will be unordered and varying in size (sometimes empty).

How can I combine these two lists to create a single unique list based on the ID?

  • If the IDs are the same, choose the one which has the latest date
  • If the IDs and dates are the same, choose the one with the higher weighted number
  • If all are the same, it doesn't matter - choose either.

My solution is very cumbersome and ugly to read - it involved two nested loops and 3 pairs of if-else statements. Is there a better way, more efficient way of doing it.

Here is some sample code:

import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

public class Scrap2 {

    public static void main(String[] args) {

        MyEntity one = new MyEntity("A", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 10);
        MyEntity two = new MyEntity("B", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 10);
        MyEntity three = new MyEntity("C", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 10);
        MyEntity four = new MyEntity("D", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 10);

        List<MyEntity> listOne = Arrays.asList(one, two, three, four);

        MyEntity aaa = new MyEntity("A", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 10);
        MyEntity bbb = new MyEntity("B", new Date(2020 - 1900, Calendar.OCTOBER, 25), 20);
        MyEntity ccc = new MyEntity("D", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 20);
        MyEntity ddd = new MyEntity("E", new Date(2020 - 1900, Calendar.SEPTEMBER, 25), 20);

        List<MyEntity> listTwo = Arrays.asList(aaa, bbb, ccc, ddd);

        for (MyEntity listItem : combineTwoLists(listOne, listTwo)) {
            System.out.println(listItem);
        }

    }

    private static List<MyEntity> combineTwoLists(List<MyEntity> listOne, List<MyEntity> listTwo) {
       
        return Arrays.asList(listOne.get(0), listTwo.get(1), listOne.get(2), listTwo.get(2), listTwo.get(3));
    }
}


class MyEntity {

    private final String id;
    private final Date date;
   private final Integer weightedNumber;

    public MyEntity(String id, Date date, Integer weightedNumber) {
        this.id = id;
        this.date = date;
        this.weightedNumber = weightedNumber;
    }

    @Override
    public String toString() {
        return "MyEntity{" +
                "id='" + id + '\'' +
                ", date=" + date +
                ", weightedNumber=" + weightedNumber +
                '}';
    }

    public String getId() {
        return id;
    }

    public Date getDate() {
        return date;
    }

    public Integer getWeightedNumber() {
        return weightedNumber;
    }
}

The output should be something like this (can be unordered/unsorted):

MyEntity{id='A', date=Fri Sep 25 2020, weightedNumber=10} 
MyEntity{id='B', date=Sun Oct 25 2020, weightedNumber=20} 
MyEntity{id='C', date=Fri Sep 25 2020, weightedNumber=10} 
MyEntity{id='D', date=Fri Sep 25 2020, weightedNumber=20} 
MyEntity{id='E', date=Fri Sep 25 2020, weightedNumber=20}
3 Answers

You can join both lists first then get the max by date then weightedNumber using Comparator with BinaryOperator.maxBy for the same id to create a map using Collectors.toMap.Then takes the value of the map in new ArrayList.

return new ArrayList<MyEntity>(
    Stream.concat(listOne.stream(), listTwo.stream())
          .collect(Collectors.toMap(MyEntity::getId, Function.identity(),
                   BinaryOperator.maxBy(Comparator.comparing(MyEntity::getDate)
                                          .thenComparing(MyEntity::getWeightedNumber))))
          .values());

The following approach using Collectors.groupingBy and Collectors.maxBy should work:

  1. Concatenate the streams of the two lists
  2. Group the list by ID using groupingBy
  3. And then group consequently by date and by weightedNumber using maxBy collector.
  4. Convert the resulting map of Map.Entry<String, Optional<MyEntity>> to List<MyEntity>
private static List<MyEntity> combineTwoLists(List<MyEntity> listOne, List<MyEntity> listTwo) {
    return Stream.concat(listOne.stream(), listTwo.stream())
                 .collect(Collectors.groupingBy(MyEntity::getId,
                          Collectors.maxBy(                                                     
                              Comparator.comparing(MyEntity::getDate)
                                        .thenComparing(MyEntity::getWeightedNumber))
                       )).entrySet() 
                 .stream()  // Stream<String, Optional<MyEntity>>
                 .map(Map.Entry::getValue) // take optional value 
                 .map(Optional::get)       // get from optional
                 .collect(Collectors.toList());
}

Here's another way to do this, without streams, yet functional:

Map<String, MyEntity> map = new LinkedHashMap<>();
Consumer<MyEntity> addAction = e -> map.merge(
        e.getId(), 
        e, 
        BinaryOperator.maxBy(Comparator.comparing(MyEntity::getDate)
                                       .thenComparing(MyEntity::getWeightedNumber)));
listOne.forEach(addAction);
listTwo.forEach(addAction);

List<MyEntity> result = new ArrayList<>(map.values());

This iterates both lists and executes the addAction consumer on each element. The addAction consumer consists of merging entities in a map, by applying the Map.merge method.

Related