pattern-A but not overlapping with pattern-B

Viewed 55

Given an arbitrary string of words (for simplicity each letter represents a word):
a b c d e f c g e b c f d g

I am trying to find a regex that allows me to match an expression (word b followed by word f with a distance of max 3 words), but not when overlapping with another expression (word c followed by word e with a distance of max 3 words).

and given the pattern-A to match b\s+(?:\S+\s+){1,3}f (would match b c d e fand b c f) and the not to overlap pattern-B c\s+(?:\S+\s+){1,3}e and pattern-C d\s+(?:\S+\s+){1,3}g
is there a combined regex to match b c f but not b c d e f because the latter is overlapping with pattern-B (c d e) or because there is a partial overlap with pattern-C (d e f c g)?

I have added the string and patterns in regex101 for convenience.

I am using the python regex module (PCRE). Any help is appreciated.

1 Answers

Whenever you want to prevent or require a match at a point according to a pattern and don't want that pattern to consume anything, lookaheads are a potential solution. Indeed, all that's needed is to put patterns B and C as negative lookaheads in the position where you want to prevent matches (i.e. immediately before a word that's between the 'b' and 'f').

An outline of the pattern:

\b(b\s+(?:(?!{pattern-B}|{pattern-C})\w+\s+){1,3}f)\b

Note word boundary anchors have been added before the first word and after the last.

With the other patterns substituted in, the whole regex becomes (in free-spacing mode):

\b(
  b\s+
  (?:
    # will prevent matching both contained and 
    # partially overlapped c-e and d-g patterns
    (?!c\s+(?:\S+\s+){1,3}e
      |d\s+(?:\S+\s+){1,3}g)
    \w+\s+
  ){1,3}
  f
|
  c\s+(?:\S+\s+){1,3}e
|
  d\s+(?:\S+\s+){1,3}g
)\b

(demo)

Lookbehinds in python's re module are limited to having a fixed length, which requires special handling to achieve some results.

Negative lookarounds won't always work as you might expect. For example, within repetitions, they can prevent matching in one position, but at that point the regex engine will in general then try other positions (possessive quantifiers being the exception), where it might succeed.

For another example, alternate negative lookarounds (e.g. ((?!A)|(?!B))) will have no effect, since a failure on one branch means the other will succeed. Instead, simply concatenate the negative lookarounds (e.g. (?!A)(?!B)). This comes up when you have negative lookbehinds of different lengths (e.g. (?<!\ba\b)(?<!\bthe\b)).

Related