Checking multiple conditions in an array - Java

Viewed 168

I'm writing a function to check multiple conditions in an array, if they are all true then return true.

For example:

public class Attribute {
    private final String key;
    private final String value;
    //...
}

boolean canContactDogOwner(List<Attribute> attributes) {
    boolean hasDog = false;
    boolean isSubscribed = false;
    boolean isOkToCall = false;

    for (var attribute : attributes) {
        if (attribute.key().equals("dogName")) {
            hasDog = true;
        } else if (attribute.key().equals("isSubscribed") && attribute.value().equals("Y")) {
            isSubscribed = true;
        } else if (attribute.key().equals("okToCall") && attribute.value().equals("Y")) {
            isOkToCall = true;
        }
        // 1.
    }

    return hasDog && isSubscribed && isOkToCall;
}

void foo() {
    List<Attribute> attributes = new ArrayList<>();
    attributes.add(new Attribute("isSubscribed", "Y"));
    attributes.add(new Attribute("okToCall", "Y"));
    attributes.add(new Attribute("mobile", "12345678"));
    attributes.add(new Attribute("landline", "1346346"));
    attributes.add(new Attribute("email", "white@email.com"));
    attributes.add(new Attribute("dogName", "Alex"));

    boolean canContact = canContactDogOwner(attributes);
}

Two questions:

  1. When all conditions are meet, the loop can be break, but if I add a check there, we would be checking every step in the loop, which doesn't look good. Any suggestions?

  2. Is there a better / concise way to do it?

Like following?

boolean canContactDogOwner(List<Attribute> attributes) {
    return attributes.stream().allMatch(A,B,C);
}
7 Answers

You can modify method canContactDogOwner to be like this,

    boolean canContactDogOwner(List<Attribute> attributes) {
        List<Attribute> conditions = new ArrayList<>();
        conditions.add(new Attribute("isSubscribed", "Y"));
        conditions.add(new Attribute("okToCall", "Y"));
            
        return attributes.containsAll(conditions) && 
                attributes.stream().anyMatch((attribute -> attribute.key.equals("dogName")));

    }

A working and cleaner approach (IMO) will be to use some abstract data type like Map in this case..

static boolean canContactDogOwner(List<Attribute> attributes){
    Map<String, String> attributeMap = new HashMap<>(); // empty map
    attributes.forEach(attr -> attributeMap.put(attr.getKey(), attr.getValue())); // populate map
    return attributeMap.containsKey("dogName") &&
           "Y".equals(attributeMap.get("isSubscribed")) &&
           "Y".equals(attributeMap.get("okToCall")); // Constant-String-first on equals check to avoid nullPointerExc with less code, yet clean
}

The code above with the comment is self-explanatory, so not adding details of the code.

But it is worth mentioning that

  • the complexity is still O(n) like other solutions here, n - number of elements (attribute objects)
  • flexibility to add or remove more conditions in the return statement
  • map as a chosen data-type and <Constant>.equals check avoids key validation and nullPointerException respectively.

If you are fascinated with Java-Streams, you can modify the code like this too..

static boolean canContactDogOwner(List<Attribute> attributes){
    Map<String, String> attributeMap = attributes.stream()
            .collect(Collectors.toMap(Attribute::getKey, Attribute::getValue));
    return attributeMap.containsKey("dogName") &&
            "Y".equals(attributeMap.get("isSubscribed")) &&
            "Y".equals(attributeMap.get("okToCall"));
}

You could check if all condition is meet only when you set a value to true, it will happen only 3 time.

And more concise way, probably with stream().anyMatch() but i'm not sure it will be more readable

Stream and allMatch(Predicate predicate) is a better way to do it in my opinion, but keep in mind that allMatch() take a Predicate as an argument, so you need to provide one.

I would suggest you encapsulate the attributes and create a class something like Owner.

public class Owner {
    private boolean isSubscribed;
    private boolean okToCall;
    private String mobile;
    private String landline;
    private String email;
    private Optional<String> dogName;

    public Owner(boolean isSubscribed, boolean okToCall, String mobile, String landline, String email, Optional<String> dogName) {
        this.isSubscribed = isSubscribed;
        this.okToCall = okToCall;
        this.mobile = mobile;
        this.landline = landline;
        this.email = email;
        this.dogName = dogName;
    }

    public boolean canContact() {
        return this.isSubscribed && this.okToCall;
    }

    public boolean hasDog() {
        return dogName.isPresent();
    }
}

This way you do not have to deal with the if loops, the Owner object will say if they have a dog and can be contacted, etc.

   public static void main(String[] args) {
        Owner owner = new Owner(true, true, "12345678", "1346346", "white@email.com", Optional.of("Alex"));
        boolean canContact = owner.hasDog() && owner.canContact();
    }

I think you can have two lists of your conditions and attributes and then check whether attributes contain all condition or not.

public static Boolean allConditionsExist(List<String> attributes, List<String> conditions) {

    return attributes.containsAll(conditions);
}

To convert your conditions and attributes to a list you can do something like this.

    List<String> conditions = Arrays.asList("dogName","isSubscribed", "okToCall"); // add all your conditions

and

    List<String> attributeKeys = attributes.stream().map(Attribute::getKey).collect(Collectors.toList());

Then call

allConditionExist(attributeKeys, conditions);

Assuming that every attribute is present only once, you could write

boolean canContactDogOwner(List<Attribute> attributes) {
    int matches = 0;


    for (var attribute : attributes) {
        if (attribute.key().equals("dogName")) ||
            attribute.key().equals("isSubscribed") && attribute.value().equals("Y") ||
            attribute.key().equals("okToCall") && attribute.value().equals("Y")) 
           {
              matches++;
              if (matches >= 3) {
                 return true;
              }
           }
    }

    return false;
}

For the stream way you could write a Collector, constructed with a list of Predicates and returning a boolean. Wouldn't be the fastest... Something like:

public class AllMatch<T> implements Collector<T, Set<Predicate<T>>, Boolean>
{

    private Set<Predicate<T>> filter;

    public AllMatch(Predicate<T>... filter)
    {
        super();
        this.filter = new HashSet(Arrays.asList(filter));
    }

    @Override
    public Supplier<Set<Predicate<T>>> supplier()
    {
        return () -> new HashSet<>();
    }

    @Override
    public BinaryOperator<Set<Predicate<T>>> combiner()
    {
        return this::combiner;
    }

    @Override
    public Set<Characteristics> characteristics()
    {
        return Stream.of(Characteristics.UNORDERED).collect(Collectors.toCollection(HashSet::new));
    }

    public Set<Predicate<T>> combiner(Set<Predicate<T>> left, Set<Predicate<T>> right)
    {

        left.addAll(right);
        return left;
    }

    public Set<Predicate<T>> accumulator(Set<Predicate<T>> acc, T t)
    {

        filter.stream().filter(f -> f.test(t)).forEach(f ->
            {
                acc.add(f);

            });
        return acc;
    }

    @Override
    public Function<Set<Predicate<T>>, Boolean> finisher()
    {
        return (s) -> s.equals(filter);
    }

    @Override
    public BiConsumer<Set<Predicate<T>>, T> accumulator()
    {
        return this::accumulator;
    }

    public static void main(String[] args) {

        Integer[] numbers = {1,2,3,4,5,6,7,8};

        System.out.println(Arrays.stream(numbers).collect(new AllMatch<Integer>((i)-> i.equals(5),(i)-> i.equals(6))));
        System.out.println(Arrays.stream(numbers).collect(new AllMatch<Integer>((i)-> i.equals(5),(i)-> i.equals(9))));

    }

}
Related