Extract n words after a pattern word - r

This is my first time attempting to extract a string using gsub and regular expressions in R. I would like to extract three words after the first occurrence of the word "at" or "around" in each cell of a text column (col in example) and place the extraction into a new column (new_extract).
What I have thus far is the following:
df$new_extract <- gsub(".*at(\\w{1,}){3}).*", "\\1", df$col, perl = TRUE)
Any advice on changes / different approaches welcomed!

Your regex attempts to match words only after the last at. Also, since there is no pattern to match the gap between at or around (you are not trying to match around at all by the way), your pattern will not extract any words in the end.
I suggest this approach with sub:
sub(".*?\\ba(?:t|round)\\W+(\\w+(?:\\W+\\w+){0,2}).*", "\\1", df$col, perl=TRUE)
See the regex demo.
Here,
.*? - matches from the start, any zero or more chars other than line break chars as few as possible
\ba - a word boundary and then a
(?:t|round) - t or round
\W+ - one or more non-word chars
(\w+(?:\\W+\\w+){0,2}) - Group 1: one or more word chars and then zero, one or two occurrences of one or more non-word chars followed with one or more word chars
.* - any zero or more chars other than line break chars as many as possible.

Related

Can quantifiers be used in regex replacement in R?

My objective would be replacing a string by a symbol repeated as many characters as have the string, in a way as one can replace letters to capital letters with \\U\\1, if my pattern was "...(*)..." my replacement for what is captured by (*) would be something like x\\q1 or {\\q1}x so I would get so many x as characters captured by *.
Is this possible?
I am thinking mainly in sub,gsub but you can answer with other libraris like stringi,stringr, etc.
You can use perl = TRUE or perl = FALSE and any other options with convenience.
I assume the answer can be negative, since seems to be quite limited options (?gsub):
a replacement for matched pattern in sub and gsub. Coerced to character if possible. For fixed = FALSE this can include backreferences "\1" to "\9" to parenthesized subexpressions of pattern. For perl = TRUE only, it can also contain "\U" or "\L" to convert the rest of the replacement to upper or lower case and "\E" to end case conversion. If a character vector of length 2 or more is supplied, the first element is used with a warning. If NA, all elements in the result corresponding to matches will be set to NA.
Main quantifiers are (?base::regex):
?
The preceding item is optional and will be matched at most once.
*
The preceding item will be matched zero or more times.
+
The preceding item will be matched one or more times.
{n}
The preceding item is matched exactly n times.
{n,}
The preceding item is matched n or more times.
{n,m}
The preceding item is matched at least n times, but not more than m times.
Ok, but it seems to be an option (which is not in PCRE, not sure if in PERL or where...) (*) which captures the number of characters the star quantifier is able to match (I found it at https://www.rexegg.com/regex-quantifier-capture.html) so then it could be used \q1 (same reference) to refer to the first captured quantifier (and \q2, etc.). I also read that (*) is equivalent to {0,} but I'm not sure if this is really the fact for what I'm interested in.
EDIT UPDATE:
Since asked by commenters I update my question with an specific example provide by this interesting question. I modify a bit the example. Let's say we have a <- "I hate extra spaces elephant" so we are interested in keeping the a unique space between words, the 5 first characters of each word (till here as the original question) but then a dot for each other character (not sure if this is what is expected in the original question but doesn't matter) so the resulting string would be "I hate extra space. eleph..." (one . for the last s in spaces and 3 dots for the 3 letters ant in the end of elephant). So I started by keeping the 5 first characters with
gsub("(?<!\\S)(\\S{5})\\S*", "\\1", a, perl = TRUE)
[1] "I hate extra space eleph"
How should I replace the exact number of characters in \\S* by dots or any other symbol?
Quantifiers cannot be used in the replacement pattern, nor the information how many chars they match.
What you need is a \G base PCRE pattern to find consecutive matches after a specific place in the string:
a <- "I hate extra spaces elephant"
gsub("(?:\\G(?!^)|(?<!\\S)\\S{5})\\K\\S", ".", a, perl = TRUE)
See the R demo and the regex demo.
Details
(?:\G(?!^)|(?<!\S)\S{5}) - the end of the previous successful match or five non-whitespace chars not preceded with a non-whitespace char
\K - a match reset operator discarding text matched so far
\S - any non-whitespace char.
gsubfn is like gsub except the replacement string can be a function which inputs the match and outputs the replacement. The function can optionally be expressed a formula as we do here replacing each string of word characters with the output of the function replacing that string. No complex regular expressions are needed.
library(gsubfn)
gsubfn("\\w+", ~ paste0(substr(x, 1, 5), strrep(".", max(0, nchar(x) - 5))), a)
## [1] "I hate extra space. eleph..."
or almost the same except function is slightly different:
gsubfn("\\w+", ~ paste0(substr(x, 1, 5), substring(gsub(".", ".", x), 6)), a)
## [1] "I hate extra space. eleph..."

using regular expressions (regex) to make replace multiple patterns at the same time in R

I have a vector of strings and I want to remove -es from all strings (words) ending in either -ses or -ces at the same time. The reason I want to do it at the same time and not consequitively is that sometimes it happens that after removing one ending, the other ending appears while I don't want to apply this pattern to a single word twice.
I have no idea how to use two patterns at the same time, but this is the best I could:
text <- gsub("[sc]+s$", "[sc]", text)
I know the replacement is not correct, but I wonder how can I show that I want to replace it with the letter I just detected (c or s in this case). Thank you in advance.
To remove es at the end of words, that is preceded with s or c, you may use
gsub("([sc])es\\b", "\\1", text)
gsub("(?<=[sc])es\\b", "", text, perl=TRUE)
To remove them at the end of strings, you can go on using your $ anchor:
gsub("([sc])es$", "\\1", text)
gsub("(?<=[sc])es$", "", text, perl=TRUE)
The first gsub TRE pattern is ([sc])es\b: a capturing group #1 that matches either s or c, and then es is matched, and then \b makes sure the next char is not a letter, digit or _. The \1 in the replacement is the backreference to the value stored in the capturing group #1 memory buffer.
In the second example with the PCRE regex (due to perl=TRUE), (?<=[sc]) positive lookbehind is used instead of the ([sc]) capturing group. Lookbehinds are not consuming text, the text they match does not land in the match value, and thus, there is no need to restore it anyhow. The replacement is an empty string.
Strings ending with "ces" and "ses" follow the same pattern, i.e. "*es$"
If I understand it correctly than you don't need two patterns.
Example:
x = c("ces", "ses", "mes)
gsub( pattern = "*([cs])es$", replacement = "\\1", x)
[1] "c" "s" "mes"
Hope it helps.
M

Regular expression to find the last hyphen, then move two spaces to the right and delete everything from there rightward

I have a series of strings that I would like to use regular expressions to compress.
1 617912568590104527563-Congress-Dem-Packages_Nomination-DC2019-08-08.xlsx
2 517912568590504527553-Dem-Plans-Packages_Debate2019-08-08.xlsx
3 47912568590104527523-Congress-Dem-Packages_House2019-08-08 (1).xlsx
I would like the result of the regular expression to be the following compressed strings:
1 Nomination-DC2019-08-08
2 Debate2019-08-08
3 House2019-08-08
Basically, the logic I'm looking for is to find the last hyphen, then move two spaces to the right and delete everything from there rightward. I'm undertaking this in R.
Update: I tried the following workflow, which addressed my issue. h/t to #brittenb for identifying the very useful tools::file_path_sans_ext()
x<-tools::file_path_sans_ext(x)
x<-str_replace(x, " .*", "")
x<-str_replace(x,".*\\_", "")
However, if anyone has a one line regex solution to this that would be great.
Update 2: h/t #WiktorStribiżew for identifying two one-liner solutions:
stringr::str_replace(x, ".*_([^.\\s]+).*", "\\1")
sub(".*_([^.[:space:]]+).*", "\\1", x)
You may simplify the task if you use tools::file_path_sans_ext() to extract the file name without extensions first and then grab all non-whitespace chars from the last _:
x <- c("617912568590104527563-Congress-Dem-Packages_Nomination-DC2019-08-08.xlsx", "517912568590504527553-Dem-Plans-Packages_Debate2019-08-08.xlsx", "47912568590104527523-Congress-Dem-Packages_House2019-08-08 (1).xlsx")
library(stringr)
str_extract(tools::file_path_sans_ext(x), "(?<=_)[^_\\s]+(?=[^_]*$)")
See the R demo. The (?<=_)[^_\\s]+(?=[^_]*$) regex matches a location after _, then matches 1+ chars other than _ and whitespaces and then asserts there are 0+ chars other than _ up to the end of string.
You may achieve what you need without extra libraries:
sub(".*_([^.[:space:]]+).*", "\\1", x)
See the regex demo and the R demo.
With stringr:
str_replace(x, ".*_([^.\\s]+).*", "\\1")
See the regex graph:
Details
.*_ - any 0+ chars as many as possible to the last occurrence of the subsequent patterns starting with _
([^.[:space:]]+) - Capturing group 1 (its value is referenced to with \1 placeholder, or replacement backrefence, from the replacement pattern): 1+ chars other than a dot and whitespace (note \s does not denote whitespace inside [...] in a TRE regex, it does in an ICU regex in stringr regex functions)
.* - any 0+ chars as many as possible.
Full code snippet:
x <- c("617912568590104527563-Congress-Dem-Packages_Nomination-DC2019-08-08.xlsx", "517912568590504527553-Dem-Plans-Packages_Debate2019-08-08.xlsx", "47912568590104527523-Congress-Dem-Packages_House2019-08-08 (1).xlsx")
sub(".*_([^.[:space:]]+).*", "\\1", x)
library(stringr)
stringr::str_replace(x, ".*_([^.\\s]+).*", "\\1")
Both yield
[1] "Nomination-DC2019-08-08" "Debate2019-08-08"
[3] "House2019-08-08"

How to extract a string between a symbol and a space?

I am trying to extract usernames tagged in a text-chat, such as "#Jack #Marie Hi there!"
I am trying to do it on the combination of # and whitespace but I cannot get the regex to match non-greedy (or at least this is what I think is wrong):
library(stringr)
str_extract(string = '#This is what I want to extract', pattern = "(?<=#)(.*)(?=\\s+)")
[1] "This is what I want to"
What I would like to extract instead is only This.
You could make your regex non greedy:
(?<=#)(.*?)(?=\s+)
Or if you want to capture only "This" after the # sign, you could try it like this using only a positive lookbehind:
(?<=#)\w+
Explanation
A positive lookbehind (?<=
That asserts that what is behind is an #
Close positive lookbehind )
Match one or more word characters \w+
The central part of your regex ((.*)) is a sequence of any chars.
Instead you shoud look for a sequence of chars other than white space
(\S+) or word chars (\w+).
Note also that I changed * to +, as you are probably not interested
in any empty sequence of chars.
To capture also a name which has "last" position in the source
string, the last part of your regex should match not only a sequence
of whitespace chars, but also the end of the string, so change
(?=\\s+) to (?=\\s+|$).
And the last remark: Actually you don't need the parentheses around
the "central" part.
So to sum up, the whole regex can be like this:
(?<=#)\w+(?=\s+|$)
(with global oprion).
Here is a non-regex approach or rather a minimal-regex approach since grep takes the detection of # through the regex engine
grep('#', strsplit(x, ' ')[[1]], value = TRUE)
#[1] "#This"
Or to avoid strsplit, we can use scan (taken from this answer), i.e.
grep('#', scan(textConnection(x), " "), value=TRUE)
#Read 7 items
#[1] "#This"

Grep in R to find words with custom "extended" boundaries

I'm looking for a regular expression to grep whole words, including words separated by digits or underscore. \\b considers digits and underscore as parts of words, not as boundaries.
For example, I'd like to catch MOUSE in "DOG MOUSE CAT", in "DOG MOUSE:CAT" but also in "DOG_MOUSE9CAT" and at the end or the beginning of an expression, as in "MOUSE9CAT" and "DOG_MOUSE". Basically, the boundary I'm looking for is any non-uppercase-alpha character plus beginning and end of line/expression (maybe missing some other cases caught by \\b here).
I've tried:
"[[0-9_]\\b]MOUSE[[0-9_]\\b]"
"[[0-9_]|\\b]MOUSE[[0-9_]|\\b]"
"[$|[^A-Z]]MOUSE[^|[^A-Z]]"
"[?<=^|[^A-Z]]MOUSE[?=$|[^A-Z]]"
None of them work.
I'm actually looking for several words (based on a long vector of values), so the final result should look something like
grep(paste("\\b", paste(searchwords, collapse = "\\b|\\b"), "\\b"), targettext)
(with a different delimiter because \\b is too restrictive for me).
(This is a similar question to the one asked by user Nick Sabbe in a comment here: Using grep in R to find strings as whole words (but not strings as part of words))
Use PCRE regex with lookarounds:
grep("(?<![A-Z])MOUSE(?![A-Z])", targettext, perl=TRUE)
See the regex demo
The (?<![A-Z]) negative lookbehind will fail the match if the word is preceded with an uppercase ASCII letter and the negative lookahead (?![A-Z]) will fail the match if the word is followed with an uppercase ASCII letter.
To apply the lookarounds to all the alternatives you have, use an outer grouping (?:...|...).
See the R online demo:
> targettext <- c("DOG MOUSE CAT","DOG MOUSE:CAT","DOG_MOUSE9CAT","MOUSE9CAT","DOG_MOUSE")
> searchwords <- c("MOUSE","FROG")
> grep(paste0("(?<![A-Z])(?:", paste(searchwords, collapse = "|"), ")(?![A-Z])"), targettext, perl=TRUE)
[1] 1 2 3 4 5

Resources