Need R code with GREP to extract numbers from string containing special characters

Viewed 407

I need R code that will extract all the numbers in a string and then sum them.

Here is a sample string: s <- c("123(11)56(10)89")

  1. Most strings will have at least nine numbers though in some cases the last value will be the letter 'x'.
  2. Double-digit numbers will be in parentheses.
  3. Not every string has a number in parentheses. Those that do could have more than one double-digit number in parentheses and their placement in a string can vary.
  4. The sample string contains nine numbers: 1,2,3,11,5,6,10,8,9.
  5. In the result string, I need to be able to access each number individually: e.g, what is the 5th number in the string? I also need to be able to sum all of a string's numbers, which for the sample string is 55.

In R, I tried to use the str_replace function, but was unable to get it to work.

I am familiar with 'dplyr'.

I reviewed stackoverflow posts that dealt with string issues, but could not find one that matched my situation. If I missed one that does, please let me know how I can view it.

6 Answers

I suppose you actually have a char scalar like this:

s <- "123(11)56(10)89"

You can use stringr. First extract all single digits \\d or two-digits surrounded by parenthesis \\(\\d{2}\\) using specific patterns collapsed by |, because R will match the longest string whenever possible. Then you may remove the ( or ) with '[()]'. Finally, convert to numeric with as.numeric():

library(stringr)

output <- s %>% str_extract_all('\\(\\d{2}\\)|\\d', simplify = TRUE) %>%
        str_remove_all('[()]')%>%
        as.numeric()

output
[1]  1  2  3 11  5  6 10  8  9

If you have a char like this s <- "c(123(11)56(10)89)", you can pipe in some preliminary transformation and use the same logic:

s %>% str_remove_all('^c\\(|\\)$')%>%
        str_extract_all('\\(\\d{2}\\)|\\d', simplify = TRUE) %>%
        str_remove_all('[()]')%>%
        as.numeric()

You can do your desired operations after that:

sum(output)
[1] 55

output[5]
[1] 5

We could use a SKIP/FAIL operation with gsub to insert a delimiter character i.e. something like a , on all digits except the the blocks inside the (..), remove the () in second gsub and read as integer values using scan

na.omit(scan(text = gsub("[()]", ",",
    gsub("\\([^)]+\\)(*SKIP)(*FAIL)|(\\d)", "\\1,", x, perl = TRUE)), 
          what = numeric(), sep=","))

-ouptut

[1]  1  2  3 11  5  6 10  8  9

Or another option is

as.integer(setdiff(strsplit(gsub("\\(\\d+\\)(*SKIP)(*FAIL)|(\\d)", "\\1(", x, perl = TRUE), "[()]")[[1]], ""))
[1]  1  2  3 11  5  6 10  8  9

data

x <- "123(11)56(10)89"

Perhaps a bit brute-force, but ...

s <- c("123(11)56(10)89")

int1 <- lapply(regmatches(s, gregexpr("\\([0-9]+\\)", s)),
               function(z) as.integer(gsub(z, pattern = "\\D", replacement = "")))
int2 <- lapply(strsplit(sapply(regmatches(s, gregexpr("[^(0-9][0-9]+\\D?|\\D?[0-9]+[^)0-9]", s)),
               function(z) paste(gsub("\\D", "", z), collapse = "")), ""), as.integer)

int1
# [[1]]
# [1] 11 10
int2
# [[1]]
# [1] 1 2 3 5 6 8 9

Then you can sum them up with

mapply(sum, int1, int2)
# [1] 55

The reason I kept them as lists is so that this can be applied on a vector of strings and the sums retained in the indices.


Edit

This can be simplified into a single expression by using GuedesBF's regex in base R. Don't accept my answer based on this (that answer does the bulk of the work), but in base R this works just as well:

lapply(regmatches(s, gregexpr("\\(\\d{2}\\)|\\d", s)),
       function(z) as.integer(gsub("\\D", "", z)))
# [[1]]
# [1]  1  2  3 11  5  6 10  8  9

If all you want is the sum without seeing this vector, then

sapply(regmatches(txt, gregexpr("\\(\\d{2}\\)|\\d", txt)),
       function(z) sum(as.integer(gsub("\\D", "", z))))
# [1] 55

Really, though, GuedesBF got the regex right for this, you should accept that answer. The only advantages this answer provides are:

  1. Base R, in case that's important to you.

  2. This answer deals with each string independently, so that if length(s) is greater than 1, this answer still works using GuedesBF's regex.

    s3 <- rep(s, 3)
    sapply(regmatches(s3, gregexpr("\\(\\d{2}\\)|\\d", s3)),
           function(z) sum(as.integer(gsub("\\D", "", z))))
    # [1] 55 55 55
    

    ... but this could easily be remedied with lapply or purrr::map or something similar.

So this must be painful for all profis, but it is my try, please let me know:

  1. extract all numbers from s
  2. then create a named vector with the above mentioned pattern.

I was not able to get this pattern bei regex so I used gsub one by one.

Please tell!

s <- as.numeric(gsub("[^0-9.-]", "", s))

my_vector <- c(digit_1 = substr(s, 1,1),
               digit_2 = substr(s, 2,2),
               digit_3 = substr(s, 3,3),
               digit_4 = substr(s, 4,5),
               digit_5 = substr(s, 6,6),
               digit_6 = substr(s, 7,7),
               digit_7 = substr(s, 8,9),
               digit_8 = substr(s, 10,10),
               digit_9 = substr(s, 11,11)
               )
my_vector

output:

digit_1 digit_2 digit_3 digit_4 digit_5 digit_6 digit_7 digit_8 digit_9 
    "1"     "2"     "3"    "11"     "5"     "6"    "10"     "8"     "9" 

If there are sometimes more than two digits in a number, you could go with a function like this one:

get_num <- function(x) {
  y <- unlist(strsplit(x,""))
  out <- numeric()
  nexting <- FALSE
  for(i in 1:length(y)) {
    
    if(!nexting) keep <- character()
    if(y[i] == "(") {
      nexting <- TRUE
      keep <- character()
      next
    } 
    if(y[i] == ")") {
      nexting <- FALSE
    } else {
      keep <- paste(keep,y[i], collapse = "")
      keep <- gsub(" ","", keep)
    }
    if(!nexting) {
      if(is.na(as.numeric(keep))) next
      out <- c(out,as.numeric(keep))
    } 
  }
  return(out)
}

Then you get the number vector from the character string and can do what you want with it from there. This also removes any unwanted characters that are not numeric or (/).

x <- "123(11)56(10)89"
get_num(x)
[1]  1  2  3 11  5  6 10  8  9
z <- "11(384)2299(298)29"
get_num(z)
[1]   1   1 384   2   2   9   9 298   2   9
a <- "23p94872(39827)20x"
get_num(a)
[1]     2     3     9     4     8     7     2 39827     2     0

It's a bit hacky, but it gets the job done.

Here's a one-liner stringr solution:

str_extract_all(x, "(?<!\\()\\d(?!\\))|(?<=\\()(\\d+)(?=\\))", "\\1")
[,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9]
"1"  "2"  "3"  "11" "5"  "6"  "10" "8"  "9"

This works by using lookarounds (one for the single digits, one for the double digits, the two separated as alternatives by |) and by referring back to the digits in-between of them using backreference with \\1

As numeric data:

as.numeric(unlist(str_extract_all(x, "(?<!\\()\\d(?!\\))|(?<=\\()(\\d+)(?=\\))", "\\1")))
[1]  1  2  3 11  5  6 10  8  9

Data:

x <- "123(11)56(10)89"
Related