Mapping str_detect over a list of strings to detect a second list of strings

Viewed 2545

Take a list of strings:

strings <- c("ABC_XZY", "qwe_xyz", "XYZ")

I'd like to get all elements in strings that don't contain a specific substring

avoid <- c("ABC")

I can do this

library(stringr)
library(dplyr)
library(purrr)

strings %>% 
   .[!map_lgl(., str_detect, avoid)]
[1] "qwe_xyz" "XYZ"

What I'd like to do though is specify several substrings

avoid_2 <- c("ABC", "qwe")

And then map over the list as before (doesn't work)

strings %>% 
   .[!map_lgl(., str_detect, avoid_2)]
Error: Result 1 must be a single logical, not a logical vector of length 2

What I want is

[1] "XYZ"

The error is clear - each element of string is generating a logical for each element of avoid_2, for total of 2 logical/element and map_lgl can only handle one/element.

I can of course do each substring separately, but I don't want to - I want to make a list of substrings

don't want, but does work

strings %>%
  .[!map_lgl(., str_detect, "ABC")] %>% 
  .[!map_lgl(., str_detect, "qwe")]
4 Answers

One option could be:

strings[map_lgl(strings, ~ !any(str_detect(., avoid_2)))]

[1] "XYZ"

Or doing directly:

strings[!str_detect(strings, paste(avoid_2, collapse = "|"))]

In addition to the answers already provided, it's worth noting that stringr::str_detect and, therefore, stringr::str_subset are vectorised over both their string and the pattern arguments. This means that you don't actually need any kind of explicit iteration (via loop, lapply, or map) or calls to paste:

library(stringr)

strings <- c("ABC_XZY", "qwe_xyz", "XYZ")
avoid_2 <- c("ABC", "qwe")

str_subset(strings, avoid_2, negate = TRUE)
#> Warning in stri_subset_regex(string, pattern, omit_na = TRUE, negate = negate, :
#> longer object length is not a multiple of shorter object length
#> [1] "XYZ"

Rather annoyingly, this generates a warning (which seems to stem from the underlying dependency on stringi::str_subset_regex). Crucially, though, it produces the expected results.

We can loop over the 'avoid_2' pattern vector instead of 'string' as 'string' argument is vectorized (if the pattern is also of the same length as 'string', then both of them can be passed for elementwise checking), then reduce the logical vector with |, negate and extract the elements from the 'strings' vector

library(dplyr)
library(stringr)
library(purrr)
avoid_2 %>% 
    map(~ str_detect(strings, .x)) %>%
    reduce(`|`) %>% `!` %>% 
    magrittr::extract(strings, .)
#[1] "XYZ"

Or using base R with grep where we can pass the invert to get the opposite values of the matching pattern

grep(paste(avoid_2, collapse="|"), strings, invert = TRUE, value = TRUE)
#[1] "XYZ"

You can paste all the avoid_2 strings together and collapse them with "|". This creates a regular expression that you can feed into discard and str_detect.

library(tidyverse)

strings <- c("ABC_XZY", "qwe_xyz", "XYZ")
avoid_2 <- c("ABC", "qwe")

avoid_2 <- avoid_2 %>% 
   paste(., collapse = "|")

avoid_2
[1] "ABC|qwe"

#discard any values in strings that are also in avoid_2
strings %>% 
    discard(str_detect(., avoid_2))
[1] "XYZ"
Related