Sahbaz
Sahbaz

Reputation: 1272

Good way to filter list distincted by property and ordered by date

I have very simple thing to do, i have list of persons like this:

[{
    name: John,
    date: 01-01-2018,
    attend: true
},
{
    name: Adam,
    date: 01-01-2018,
    attend: false
},
{
    name: Adam,
    date: 01-02-2018,
    attend: true
},
{
    name: JOHN,
    date: 01-02-2018,
    attend: false
}]

Result of this array should be: Adam (true), John (false)

So i need to return list of latest entries from users, in this case John first confirmed that he is attending, then he changed his mind and told that he is not attending so i'm returning his last entry (note that sometimes it's written JOHN and sometimes John but it is the same person, that is kind of tricky part)

My question what is best approach to filter out this kind of list, i was thinking applying "unique by property java stream" but first will need to order persons by date descending and names (to upper/lower case) and then i would need somehow to take latest entry.

Anyone have any good idea what is best approach to this ?

Upvotes: 7

Views: 1947

Answers (4)

fps
fps

Reputation: 34460

A compact way without streams:

Map<String, User> map = new LinkedHashMap<>();
users.forEach(u -> map.merge(
        u.getName().toLowerCase(), 
        u, 
        BinaryOperator.maxBy(Comparator.comparing(Person::getDate))));

Collection<User> result = map.values();

Or if you do need a List:

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

This code uses Map.merge, which puts the entry in the map if there's no entry with the same key (the lowercased user name), or, if the map already contains an entry for the key, it applies the merge function, which in this case chooses the User instance with the max date.

Upvotes: 1

Leandro Lima
Leandro Lima

Reputation: 1170

Despite all answers until now are functionally correct, please consider this option:

final Map<String, Boolean> lastAttendResults =
            people
                .stream()
                .collect(
                    groupingBy(
                        Person::getName, // Define what is the property you are using to group people.
                        () -> new TreeMap<String, Boolean>(String.CASE_INSENSITIVE_ORDER), // Supply a map implementation that ignore name case.
                        collectingAndThen(maxBy(Comparator.comparing(Person::getDate)), // Foreach list of grouped people, select that with last date.
                            o -> o.get().isAttend()))); // As maxBy returns an Optional<Person> and we are sure that it exists, just get the Person and if he attends.

This implementation is interesting because it let be evident the concept of collection grouping. Although, in depth, it also uses a map, seems to me that use or not a map is not programmer problem, I mean, what we are looking for is how to group people by name and then get the last entry.

If you prefer receive a List of Person instead of a Map of name and attend, you can use:

final List<Person> lastAttendResults =
        new ArrayList<>(people
            .stream()
            .collect(
                groupingBy(Person::getName, // Define what is the property you are using to group people.
                    () -> new TreeMap<String, Person>(String.CASE_INSENSITIVE_ORDER), // Supply a map implementation that ignore name case.
                    collectingAndThen(maxBy(Comparator.comparing(Person::getDate)), // Foreach list of grouped people, select that with last date.
                        Optional::get // As maxBy returns an Optional<Person> and we are sure that is exists, just get the Person.
                        ))).values());

Upvotes: 2

Naman
Naman

Reputation: 31978

You can use Collectors.toMap to do the same as:

List<Person> finalList = new ArrayList<>(people.stream()
        .collect(Collectors.toMap(a -> a.getName().toLowerCase(),  // name in lowercase as the key of the map (uniqueness)
                Function.identity(), // corresponding Person as value
                (person, person2) -> person.getDate().isAfter(person2.getDate()) ? person : person2)) // merge in case of same name based on which date is after the other
        .values()); // fetch the values

Note: The above assumes the minimal Person class to be

class Person {
    String name;
    java.time.LocalDate date;
    boolean attend;
    // getters and setters
}

Upvotes: 8

Ousmane D.
Ousmane D.

Reputation: 56453

You could use the toMap collector:

Collection<Person> values = source.stream()
                    .collect(toMap(e -> e.getName().toLowerCase(),
                            Function.identity(),
                            BinaryOperator.maxBy(Comparator.comparing(Person::getDate))))
                    .values();

see this answer for an explanation as to how toMap works

Upvotes: 5

Related Questions