How to replace specific trailing characters but block the first two letters

Viewed 88

I want to harmonize strings by replacing all trailing Xs and Ys with underscores. Since the length of those strings varies, I wrote the following regex which works fine. However, the first two characters should always stay as they are. I know that I could use substr() and paste0() as a workaround but how can I include this "do not replace the first two characters" in the regex?

x <- c("AXZ", "AZXYYX", "HZX_Y", "BXX", "XYX_")

# replaces all trailing X / Y
gsub("[XY](?=[XY_]*$)", "_", x, perl = TRUE)
#> [1] "AXZ"    "AZ____" "HZ___"  "B__"    "____"

# blocks first character
gsub("(?<!^)[XY](?=[XY_]*$)", "_", x, perl = TRUE)
#> [1] "AXZ"    "AZ____" "HZ___"  "B__"    "X___"

# desired output
c("AXZ", "AZ____", "HZ___", "BX_", "XY__")
#> [1] "AXZ"    "AZ____" "HZ___"  "BX_"    "XY__"

I already managed to exclude the first letter so I guess it should be an easy fix.

3 Answers

The following approach seems to work:

gsub("(?<=.{2})[XY](?=[XY_]*$)", "_", x, perl=TRUE)

[1] "AXZ"    "AZ____" "HZ___"  "BX_"    "XY__"

Here is an explanation of the regex pattern, which uses lookarounds to enforce the correct replacements:

(?<=.{2})        lookbehind and assert there exist at least 2 preceding characters;
                 this ensures replacement will never be made on first 2 characters
[XY]             match any of X or Y
(?=[XY_]*$)      lookahead and assert that previous X/Y/_ is only followed
                 by more X/Y/_ until the end of the string

Note that we replace one character at a time with underscore, but we use gsub so that all necessary replacements will happen.

You may just skip the first two characters using (*SKIP)(*FAIL):

x <- c("AXZ", "AZXYYX", "HZX_Y", "BXX", "XYX_")

gsub("^.{2}(*SKIP)(*FAIL)|[XY](?=[XY_]*$)", "_", x, perl = TRUE)

Which yields

[1] "AXZ"    "AZ____" "HZ___"  "BX_"    "XY__"  

See a demo on regex101.com.

One way to do it is to capture the preceding two characters and repeat them. You can use '\1', '\2' etc. in the substitution string to refer to the 1st, 2nd, etc. capture groups respectively. Here we have only one capture group.

sub("(..)[XY]+$)", "\\1_", x, perl = TRUE)
Related