Regex expression to match every nth occurence of a pattern

Viewed 211

Consider this string,

str = "abc-de-fghi-j-k-lm-n-o-p-qrst-u-vw-x-yz"

I'd like to separate the string at every nth occurrence of a pattern, here -:

f(str, n = 2)
[1] "abc-de" "fghi-j" "k-lm" "n-o"...

f(str, n = 3)
[1] "abc-de-fghi" "j-k-lm" "n-o-p" "qrst-u-vw"...

I know I could do it like this:

spl <- str_split(str, "-", )[[1]]
unname(sapply(split(spl, ceiling(seq(spl) / 2)), paste, collapse = "-"))
[1] "abc-de" "fghi-j" "k-lm"   "n-o"    "p-qrst" "u-vw"   "x-yz" 

But I'm looking for a shorter and cleaner solution

What are the possibilities?

4 Answers

What about the following (where 'n-1' is a placeholder for a number):

(?:[^-]*(?:-[^-]*){n-1})\K-

See an online demo


  • (?: - Open 1st non-capture group;
    • [^-]* - Match 0+ characters other hyphen;
    • (?: - Open a nested 2nd non-capture group;
      • -[^-]* - Match an hyphen and 0+ characters other than hyphen;
      • ){n} - Close nested non-capture group and match n-times;
    • ) - Close 1st non-capture group;
  • \K- - Forget what we just matched and match the trailing hyphen.

Note: The use of \K means we must use PCRE (perl=TRUE)


To create the 'n-1' we can use sprintf() functionality to use a variable:

str <- "abc-de-fghi-j-k-lm-n-o-p-qrst-u-vw-x-yz"
for (n in 1:10) {
  print(strsplit(str, sprintf("(?:[^-]*(?:-[^-]*){%s})\\K-", n-1), perl=TRUE)[[1]])
}

Prints:

enter image description here

You could use str_extract_all with the pattern \w+(?:-\w+){0,2}, for instance to find terms with 3 words and 2 hyphens:

str <- "abc-de-fghi-j-k-lm-n-o-p-qrst-u-vw-x-yz"
n <- 2
regex <- paste0("\\w+(?:-\\w+){0,", n, "}")
str_extract_all(str, regex)[[1]]

[1] "abc-de-fghi" "j-k-lm"      "n-o-p"       "qrst-u-vw"   "x-yz"

n <- 3
regex <- paste0("\\w+(?:-\\w+){0,", n, "}")
str_extract_all(str, regex)[[1]]

[1] "abc-de-fghi-j" "k-lm-n-o"      "p-qrst-u-vw"   "x-yz"

1) gsubfn gsubfn in the package of the same name is like gsub except that the replacement can be a function, list or proto object. In the case of a proto object one can supply a fun method which has a built in count variable that can be used to distinguish the occurrences. For each match the match is passed to fun and replaced with the output of fun.

We use the input shown in the Note at the end and also n to specify the number of components to use in each element of the result and sep to specify a character that does not appear in the input.

gsubfn replaces every n-th minus with sep and the strsplit splits on that.

No complex regular expressions are needed.

library(gsubfn)

n <- 3
sep <- " "

p <- proto(fun = function(., x) if (count %% n) "-" else sep)
strsplit(gsubfn("-", p, STR), sep)
## [[1]]
## [1] "abc-de-fghi" "j-k-lm"      "n-o-p"       "qrst-u-vw"   "x-yz"       
##
## [[2]]
## [1] "abc-de-fghi" "j-k-lm"      "n-o-p"       "qrst-u-vw"   "x-yz" 

2) rollapply Another approach is to split on every - and the paste it together again using rollapply giving the same result as in (1).

library(zoo)

roll <- function(x) rollapply(x, n, by = n, paste, collapse = "-",
  partial = TRUE, align = "left")
lapply(strsplit(STR, "-"), roll)

Note

# input
STR = "abc-de-fghi-j-k-lm-n-o-p-qrst-u-vw-x-yz"
STR <- c(STR, STR)

another approach: First split on every split-pattern found, then paste/collapse into groups of n-length, using the split-pattern-variable as collapse character.

str <- "abc-de-fghi-j-k-lm-n-o-p-qrst-u-vw-x-yz"
n <- 3
pattern <- "-"

ans <- unlist(strsplit(str, pattern))
sapply(split(ans, 
             ceiling(seq_along(ans)/n)), 
       paste0, collapse = pattern)
# "abc-de-fghi"      "j-k-lm"       "n-o-p"   "qrst-u-vw"        "x-yz" 
Related