Home > OS >  Validate All or No Strings are Populated in Java
Validate All or No Strings are Populated in Java

Time:10-07

I have a need to validate several strings and ensure that either every single string is populated or none of them are.

I'm receiving the data, so I am not in control of it, and non-populated could be null or empty strings (Strings with just whitespace are counted as populated).

I can obviously write something like:

String s1 = "A";
String s2 = "B";
String s3 = "C";

boolean valid = (
        (s1 == null || s1.isEmpty()) && 
        (s2 == null || s2.isEmpty()) && 
        (s3 == null || s3.isEmpty())
    ) || (
        (s1 != null && !s1.isEmpty()) && 
        (s2 != null && !s2.isEmpty()) && 
        (s3 != null && !s3.isEmpty())
);

but that is getting hard to read and I think there are going to end up being seven strings I need to check.

so I started looking at streams and I can do

boolean anyPopulated = Stream.of(s1, s2, s3).anyMatch(s -> s != null && !s.isEmpty());
boolean allPopulated = Stream.of(s1, s2, s3).allMatch(s -> s != null && !s.isEmpty());
boolean valid = (anyPopulated == allPopulated);

Which is a bit easier to read but I can't help thinking there must be an easier way to do this that is readable.

I need to use standard Java for this so no libraries.

CodePudding user response:

At least only one stream involved

public static boolean validate(String... strings) {
    long populated = Stream.of(strings).filter(s -> s != null && !s.isEmpty()).count();
    return populated == 0 || populated == strings.length;
}

CodePudding user response:

allMatch() noneMatch()

Regarding your stream-based solution, you would advise using a combination allMatch() noneMatch() and allPopulated || nonePopulated.

Condition allPopulated || nonePopulated is more intuitive than anyPopulated == allPopulated and requires less effort to read.

It might look like there are two iterations, but keep in mind that operations allMatch() and noneMatch() are short circuit and therefore none of the two streams would iterate over the whole data set.

boolean nonePopulated = Stream.of(s1, s2, s3)
    .noneMatch(s -> s != null && !s.isEmpty());
boolean allPopulated = Stream.of(s1, s2, s3)
    .allMatch(s -> s != null && !s.isEmpty());

boolean valid = allPopulated || nonePopulated;

Note that also possible to keep the solution short-circuit and utilize only a single stream. To understand the logic, let's first have a look at the implementation with a plain for-loop.

Short-circuit solution - For-loop

To implement short-circuit logic using a loop, we can evaluate the predicate for the very first element and check all subsequent outcomes against the base result. After the first outcome that doesn't match base one, we can break the loop.

boolean isValid = validateAll(s -> s != null && !s.isEmpty(), s1, s2, s3);
public static <T> boolean validateAll(Predicate<T> pred, T... args) {
    
    if (args.length == 0) return true; // or throw

    boolean base = pred.test(args[0]);

    for (int i = 1; i < args.length; i  ) {
        if (base != pred.test(args[i])) {
            return false;
        }
    }

    return true;
}

Short-circuit solution - A single Stream

Similarly to the previous solution, we can start with evaluating the predicate for the first element. But instead of storing the result, we can adjust the predicate itself and further apply either negated predicated or initial predicate.

This stream can terminate starting from the second element (if the data is invalid).

public static <T> boolean validateAll(Predicate<T> pred, T... args) {
    
    if (args.length == 0) return true; // or throw

    Predicate<T> adjustedPredicate = pred.test(args[0]) ? pred : pred.negate();

    return Arrays.stream(args).skip(1).allMatch(adjustedPredicate);
}

There are options on how to perform this validation using a single stream, and even one statement. Some of them are listed below, but that they are not short-circuit, less space-efficient and require more effort to read.

Single statement - Built-in Collectors

Here's a solution based on the combination of collectors collectingAndThen() and partitioningBy() that would allow obtaining isValid value in a single statement:

boolean isValid = Stream.of(s1, s2, s3)
    .collect(Collectors.collectingAndThen(
        Collectors.partitioningBy(s -> s != null && !s.isEmpty()),
        map -> map.get(false).isEmpty() || map.get(true).isEmpty()
    ));

One stream - Custom Collector

If we generalize the task, it can be tackled using a custom collector, which makes uses a boolean array as the accumulation type (similar approaches you can in implementation's of built-in collectors from the Collectors class).

To create a custom collector, we can make use of the static method Collector.of().

boolean isValid = validateAll(s -> s != null && !s.isEmpty(), s1, s2, s3);

Generic method, which expects a varargs of T and a Predicate<T>:

public static <T> boolean validateAll(Predicate<T> pred, T... args) {
    
    return Arrays.stream(args).collect(allMatchOrNoneMatch(pred));
}

A custom collector, which produces a boolean value:

public static <T> Collector<T, ?, Boolean> allMatchOrNoneMatch(Predicate<T> pred) {
    
    return Collector.of(
        () -> new Boolean[]{null, true},
        (Boolean[] arr, T next) -> {
            if (arr[0] == null) arr[0] = pred.test(next);
            else if (arr[1] && (pred.test(next) != arr[0])) arr[1] = false;
        },
        (left, right) -> {
            left[1] = left[1] && right[1];
            return left;
        },
        arr -> arr[1]
    );
}

CodePudding user response:

Taking inspiration from Alexander Ivanchenko's excellent answer, I would like to shorten it slightly by inverting the condition so it checks for all or none empty (which gives the same results) and putting it into a method:

private static boolean allOrNonePopulated(String... strings) {
    return Stream.of(strings).allMatch(s -> s == null || s.isEmpty())
        || Stream.of(strings).noneMatch(s -> s == null || s.isEmpty());
}
  • Related