user1692342
user1692342

Reputation: 5237

Filter values from a list based on priority

I have a list of valid values for a type:

Set<String> validTypes = ImmutableSet.of("TypeA", "TypeB", "TypeC");

From a given list I want to extract the first value which has a valid type. In this scenario I would write something of this sort:

public class A{
    private String type;
    private String member;
}

List<A> classAList;
classAList.stream()
    .filter(a -> validTypes.contains(a.getType()))
    .findFirst();

However I would like to give preference to TypeA, i.e. if classAList has TypeA and TypeB, I want the object which has typeA. To do this one approach I've is:

Set<String> preferredValidTypes = ImmutableSet.of("TypeA");
classAList.stream()
    .filter(a -> preferredValidTypes.contains(a.getType()))
    .findFirst()
    .orElseGet(() -> {
        return classAList.stream()
        .filter(a -> validTypes.contains(a.getType()))
        .findFirst();
    }

Is there a better approach?

Upvotes: 11

Views: 3727

Answers (7)

MC Emperor
MC Emperor

Reputation: 22997

One can, of course, define two lists, one with all valid types, and one with the preferred types.

However, here is another approach. Define one list, or actually, a Map, with the keys being the valid types, and the boolean values being whether the type is preferred.

Map<String, Boolean> validTypes = ImmutableMap.of(
    "TypeA", false,
    "TypeB", false,
    "TypeC", true
);

Using AtomicReference

One option is the following:

AtomicReference<A> ref = new AtomicReference<>();
listOfAs.stream()
    .filter(t -> validTypes.containsKey(t.getType()))
    .anyMatch(t -> validTypes.get(ref.updateAndGet(u -> t).getType()));

AtomicReference now contains a preferred A if available, or another valid A, or if the stream is empty, then it contains null. This stream operation short-circuits if an A with a preferred type is found.

The drawback of this option is that it creates side-effects, which is discouraged.

Using distinct()

Another suggestion would be the following. It uses the same map structure, using a boolean to indicate which values are preferred. However, it does not create side effects.

Map<Boolean, A> map = listOfAs.stream()
    .filter(t -> validTypes.containsKey(t.getType()))
    .map(t -> new Carrier<>(validTypes.get(t.getType()), t))
    .distinct()
    .limit(2)
    .collect(Collectors.toMap(Carrier::getKey, Carrier::getValue));

It works as follows.

  1. filter discards any element that is not a valid type.
  2. Then, each element is mapped to a Carrier<Boolean, A> instance. A Carrier is a Map.Entry<K, V> which implements its equals and hashCode methods regarding only the key; the value does not matter. This is necessary for the following step,
  3. distinct(), which discards any duplicate element. This way, only one preferred type and only one valid type is found.
  4. We limit the stream to have 2 elements, one for each boolean. This is because the stream, which is lazy, stops evaluating after both booleans are found.
  5. At last, we collect the Carrier elements into a Map.

The map contains now the following elements:

  • Boolean.TRUE => A with a preferred type
  • Boolean.FALSE => A with a valid type

Retrieve the appropriate element using

A a = map.getOrDefault(true, map.get(false)); // null if not found

Upvotes: 1

Conffusion
Conffusion

Reputation: 4475

i don't like the answers already given which use Comparator. Sorting is an expensive operation. You can do it with one walk through the list. Once you find a preferred value, you can break out, otherwise you continue to the end to find a valid.

In this case anyMatch can provide the possibility to break out from the stream processing:

MyVerifier verifier=new MyVerifier(validTypes,preferredValidTypes);
classAList.stream()
    .anyMatch(verifier);
System.out.println("Preferred found:"+verifier.preferred);
System.out.println("Valid found:"+verifier.valid);

public static class MyVerifier implements Predicate<A> {
    private Set<String> validTypes;
    private Set<String> preferredValidTypes;
    A preferred=null;
    A valid=null;

    public MyVerifier(Set<String> validTypes, Set<String> preferredValidTypes) {
        super();
        this.validTypes = validTypes;
        this.preferredValidTypes = preferredValidTypes;
    }

    @Override
    public boolean test(A a) {
        if(preferred==null && preferredValidTypes.contains(a.getType())) {
            preferred=a;
            // we can stop because we found the first preferred
            return true;
        } else if(valid==null && validTypes.contains(a.getType())) {
            valid=a;
        }
        return false;
    }

}

Upvotes: 1

Hadi
Hadi

Reputation: 17299

If you have preferred valid types in other collection so you can follow this code.

Map<String,A> groupByType = classAList
           .stream()
            /* additional filter to grouping by valid types.*/
          //.filter(a->validTypes.contains(a.getType())) 
           .collect(Collectors.toMap(A::getType, Function.identity(),(v1, v2)->v1));

then use:

A result = preferredValidTypes
            .stream()
            .map(groupByType::get)
            .findFirst()
            .orElseThrow(RuntimeException::new);

or just group by preferred valid types

 A result2 = classAList
            .stream()
            .filter(a -> preferredValidTypes.contains(a.getType()))
            .collect(Collectors.toMap(A::getType, Function.identity(), (v1, v2) -> v1))
            .entrySet()
            .stream()
            .findFirst()
            .map(Map.Entry::getValue)
            .orElseThrow(RuntimeException::new);

Upvotes: 0

Oleg Cherednik
Oleg Cherednik

Reputation: 18245

In Java8 you can use streams:

public static Carnet findByCodeIsIn(Collection<Carnet> listCarnet, String codeIsIn) {
    return listCarnet.stream().filter(carnet -> codeIsIn.equals(carnet.getCodeIsin())).findFirst().orElse(null);
}

Additionally, in case you have many different objects (not only Carnet) or you want to find it by different properties (not only by cideIsin), you could build an utility class, to ecapsulate this logic in it:

public final class FindUtils {
    public static <T> T findByProperty(Collection<T> col, Predicate<T> filter) {
        return col.stream().filter(filter).findFirst().orElse(null);
    }
}

public final class CarnetUtils {
    public static Carnet findByCodeTitre(Collection<Carnet> listCarnet, String codeTitre) {
        return FindUtils.findByProperty(listCarnet, carnet -> codeTitre.equals(carnet.getCodeTitre()));
    }

    public static Carnet findByNomTitre(Collection<Carnet> listCarnet, String nomTitre) {
        return FindUtils.findByProperty(listCarnet, carnet -> nomTitre.equals(carnet.getNomTitre()));
    }

    public static Carnet findByCodeIsIn(Collection<Carnet> listCarnet, String codeIsin) {
        return FindUtils.findByProperty(listCarnet, carnet -> codeIsin.equals(carnet.getCodeIsin()));
    }
}

Upvotes: 0

Eugene
Eugene

Reputation: 120878

Well you have to take care into account that sorting is stable, that is equal elements will appear in the same order as the initial source - and you need that to correctly get the first element from that List<A> that will satisfy your requirement, thus:

String priorityType = "TypeB";

Stream.of(new A("TypeA", "A"),
          new A("TypeB", "B"),
          new A("TypeC", "C"))
      .sorted(Comparator.comparing(A::getType, Comparator.comparing(priorityType::equals)).reversed())
      .filter(x -> validTypes.contains(priorityType))
      .findFirst()
      .orElseThrow(RuntimeException::new);

Upvotes: 0

Naman
Naman

Reputation: 31938

You can use a custom comparator like:

Comparator<A> comparator = (o1, o2) -> {
    if (preferredValidTypes.contains(o1.getType()) && !preferredValidTypes.contains(o2.getType())) {
        return 1;
    } else if (!preferredValidTypes.contains(o1.getType()) && preferredValidTypes.contains(o2.getType())) {
        return -1;
    } else {
        return 0;
    }
};

to sort the list and then findFirst from that list with your conditiion.

Upvotes: 2

Adrian
Adrian

Reputation: 3134

filter list by type, order by type, collect to list, then just get first element

List<A> collect = classAList.stream()
                  .filter(a -> validTypes.contains(a.getType()))
                  .sorted(Comparator.comparing(A::getType))
                  .collect(Collectors.toList());
System.out.println(collect.get(0));

Upvotes: 4

Related Questions