How do I search for a list of strings inside another string?

Viewed 202

Here is some code that works, but looks inelegant. What is a better way to search for any occurrence of these strings inside another string?

String AndyDaltonInjury = "broken right thumb";

if (AndyDaltonInjury.toLowerCase().contains("broken") &&
    (AndyDaltonInjury.toLowerCase().contains("knee") ||
    AndyDaltonInjury.toLowerCase().contains("leg")   ||
    AndyDaltonInjury.toLowerCase().contains("ankle") ||
    AndyDaltonInjury.toLowerCase().contains("thumb") ||
    AndyDaltonInjury.toLowerCase().contains("wrist"))) 
{
    System.out.println("Marvin sends in the backup quarterback.");  
}
5 Answers

Use the Set collection and its method Set::contains insde streaming the split array with the space (" ") delimiter:

Set<String> set = new HashSet<>(Arrays.asList("knee", "leg", "ankle", "thumb", "wrist"));

String lower = "broken right thumb".toLowerCase();
String split[] = lower.split(" ");
if (lower.contains("broken") && Arrays.stream(split).anyMatch(set::contains)) {
    System.out.println("Marvin sends in the backup quarterback.");
}

Moreover, I highly recommend you to use lower-cased variable names.

As an alternative to an already posted Set-based solution (which I find better by the way, in the sense of readability), this can be done using a regular expression:

final Pattern brokeStuffPattern = Pattern.compile(
    ".*\\bbroken?\\b.*\\b(?:knee|leg|ankle|thumb|wrist)s?\\b.*"
    + "|.*\\b(?:knee|leg|ankle|thumb|wrist)s?\\b.*\\bbroken?\\b.*",
    Pattern.CASE_INSENSITIVE
);
if (brokeStuffPattern.matcher(AndyDaltonInjury).matches()) {
    ...
}

This would account for plurals and the verb's perfect tense as well, e.g. if would match "broken legs".

You could create the missing functions (contains all/any) as methods, or express them using Lambda notations:

BiPredicate<String, List<String>> containsAll = (text, words) -> 
    words.stream().allMatch(word -> text.toLowerCase().contains(word));
BiPredicate<String, List<String>> containsAny = (text, words) -> 
    words.stream().anyMatch(word -> text.toLowerCase().contains(word));

if (containsAll.test(AndyDaltonInjury, Arrays.asList("broken")) && 
    containsAny.test(AndyDaltonInjury, Arrays.asList("knee", "leg", "ankle", "thumb", "wrist"))) {
    System.out.println("Marvin sends in the backup quarterback.");
}

You can try this:

String test = "broken right thumb";

Predicate << ? super String > machCriteria = s - > Stream.of("knee", "leg", "ankle", "thumb", "wrist").anyMatch(e - > e.equals(s.toLowerCase()));
String result = Pattern.compile(" ").splitAsStream(test).anyMatch(machCriteria) ? "Marvin sends in the backup quarterback." : "";
System.out.println(result);

Hash based algorithms are likely going to give you better performance if you need to check a lot of text against occurrences within a huge set.

HashSet would be a good first attempt as the search (test if key contained within the set) will be between O(1) and O(n).

However, I would strongly advise looking into the benefit of employing a [Bloom Filter][1]. It will serve well as a prefilter as it gives a predictable performance of O(k). Because the filter will have a small % of false positive, you will need to run a second stage as well.

Look into the Guava BloomFilter for a good implementation.

Another benefit of the Bloom Filter is that it does not contain the original dataset, just a reduced hash, meaning that its size is minimal. This means that it is more suitable for distributed systems as it copies over very efficiently. In an environment like Apache Spark, you would even set this up as a Broadcast variable, as once produced it is typically constant in time.

Related