0

I've a value object whose structure is like:

class MyValueObject {
    String travellerName;
    List<String> countryTags;

    // Getters and setters
}

In this I'm maintaining a list of traveller and which countries they've visited. e.g.

Richard -> India, Poland, Australia

John -> US, Australia, Maldives

Emma -> US

Now I'm adding a filter feature, which will give list of all travellers who've visited selected countries.

List of countries will be provided as an input.

List<String> countryFilter;

This filter has a multiselect option.

The filtered result should contain both AND & OR results.

That is, if input is US & Australia, result will be:

John -> US, Australia, Maldives

Richard -> India, Poland, Australia

Emma -> US

The result should be sorted in manner that AND matches should be shown above OR matches.

Question:

How should I sort the matches?

Shall I write a comparator? If yes, some example will be very helpful.

Please suggest.

Thanks.

sonic boom
  • 764
  • 4
  • 11
  • 26
  • Does this answer your question? [How to sort List of objects by some property](https://stackoverflow.com/questions/5805602/how-to-sort-list-of-objects-by-some-property) – dawis11 Apr 26 '20 at 15:49
  • @dawis11, it is sorting on multiple fields whereas I want to sort most matches within given input list. – sonic boom Apr 26 '20 at 15:54
  • 1
    Note: `countryTags` should be a `Set`. – MC Emperor Apr 26 '20 at 18:52

3 Answers3

1

Try this:

Building the objects.

List<Traveler> travelers = new ArrayList<>();
Traveler t1 = new Traveler();
t1.traveler = "Richard";
t1.countries = List.of("India", "Poland", "Australia");     
travelers.add(t1);

t1 = new Traveler();
t1.traveler = "John";
t1.countries = List.of("US", "Australia", "Maldives");
travelers.add(t1);


t1 = new Traveler();        
t1.traveler = "Emma";       
t1.countries = List.of("US");       
travelers.add(t1);

The filter

List<String> countryFilter = List.of("US", "Maldives");

The test predicates. These are the heart of the matter.

  • they both stream countryFilter and check to see if those
    countries are in the list of traveled countries.
  • or returns true if the travelers countries contain at least one country
  • and returns true if the traveler's countries contain all the countries.
Predicate<Traveler> or = t -> countryFilter.stream()
                .anyMatch(cc -> t.countries.contains(cc));

Predicate<Traveler> and = t -> countryFilter.stream()
                .allMatch(cc -> t.countries.contains(cc));

Now just stream just stream the traveler objects
and apply the filters. Then prints the results.

System.out.println("Filtering on: " + countryFilter);
System.out.println("\nVisited all of the countries");
travelers.stream().filter(and)
    .forEach(t -> System.out.println(t.traveler));
System.out.println("\nVisited some of the countries");
travelers.stream().filter(or)
    .forEach(t -> System.out.println(t.traveler));

Prints

Filtering on: [US, Maldives]

Visited all of the countries
John

Visited some of the countries
John
Emma

The class supporting class

class Traveler {
    String traveler;
    List<String> countries;

    public String toString() {
        return traveler + " => " + countries.toString();
    }
}
WJS
  • 36,363
  • 4
  • 24
  • 39
  • Thanks, but what about cases in between. Say filter has 3 values - US, India, Maldives. Will it handle cases like all 3 match, any 2 match & only one match? – sonic boom Apr 26 '20 at 19:03
  • 1
    No.. It was AND and OR which implies all match to be true or any match to be true. Sorry if it doesn't work for you. – WJS Apr 26 '20 at 19:43
  • One thing you can do is construct other Predicates. And they can be chained and negated. So you could do something like (and.negate().or and so forth. You can even create others. You may want to read up on the `Predicate` interface to see the options. – WJS Apr 26 '20 at 20:01
0

Instead of sorting, iterate twice, once for the AND matches and once for the OR matches:

// Say you have a list of MyValueObject type called travelers
ArrayList<MyValueObject> copyTravelers = new ArrayList<>(travelers);
List<MyValueObject> filtered = new ArrayList<>();

// AND
for (MyValueObject t : copyTravelers) {
    MyValueObject traveler = t;
    for (String country : countryFilter)
        if (!traveler.countryTags.contains(country)) {
            traveler = null;
            break;
        }
    if (traveler != null) {
        filtered.add(traveler);
        copyTravelers.remove(traveler);
    }
}

// OR
for (MyValueObject t : copyTravelers) {
    for (String country : countryFilter)
        if (traveler.countryTags.contains(country)) {
            filtered.add(t);
            copyTravelers.remove(t);
            break;
        }
}

0

i tried an approach where you get an ordered Map of country matches:

public Map<Long, String> sortMatches(List<MyValueObject> travellers, List<String> countries){

        TreeMap<Long, List> collect = travellers.stream()
                .flatMap(t -> t.countries.stream()
                        .filter(c -> countries.contains(c))
                        .map(c -> t.getTravellerName())
                        .collect(groupingBy(Function.identity(), counting()))
                .entrySet()
                .stream()
                ).collect(groupingBy(
                        e -> e.getValue(),
                        TreeMap::new,
                        listCollector
                ));
        return collect;
}

first the matches are counted and written to a temporary Map<String, Long> where the String is the travellerName and as value there is the match count of this traveller.

in a second step a new map is created which stores as key the amount of matches and as values the travellerNames of the travellers who have visited so many countries. therefore, the listCollector is used:

Collector<Map.Entry, List, List> listCollector =
    Collector.of(ArrayList::new,
                (l, e) -> l.add(e.getKey()), 
                (l1, l2) -> { l1.addAll(l2); return l1;});
pero_hero
  • 2,881
  • 3
  • 10
  • 24