Find multiple matches starting with number followed by unique occurrence of specific words

Viewed 33

I have the following regex problem:

The string is: 32 Abs. 8 und 10, 37 Abs. 5 Satz 3, 39 Abs. 3 und 40 Abs. 7, 8 und 10

I want to get the output: ['32 Abs. 8 und 10', '37 Abs. 5 Satz 3', '39 Abs. 3', '40 Abs. 7, 8 und 10']

The number after und should count to the previous part, unless the number is followed by repeating Abs., Satz or Nr.. The words Abs., Satz or Nr. should only occur once after a number.

With the regex: (\d+(?: (?:Abs.|Satz|Nr.)* \d+(?:(?:, \d+)* und (?!Abs.SatzNr.) \d+)?)*)

I get: ['32 Abs. 8', '10', '37 Abs. 5 Satz 3', '39 Abs. 3', '40 Abs. 7', '8', '10']

1 Answers

Since the language isn't fully specified in the question, I'm assuming a language with the grammar:

root = phrase | phrase "," root
phrase = clause [ "und" clause ]
clause = number conj (numbers | number conj number)
conj = "Abs." | "Satz" | "Nr."
numbers = number | number "und" number | number "," numbers

Note: this grammar isn't necessarily unambiguous. Proof or disproof left as an exercise.

The start of a phrase is simple: /\d+ (Abs\.|Nr\.|Satz)/. The complex parts are the rest of a phrase (i.e. the pattern following the first abbreviation in a phrase) and separating the phrases. The latter seems to be presenting the most trouble.

The end of a phrase can be recognized with one observation: it's always a number followed by either a comma, "und", or the end of the string, and never an abbreviation. If it's possible to write a regex matching the tail of a phrase that doesn't consume the phrase-separator, a simple (, | und |$) should match the separator, as it won't match an abbreviation that would indicate the preceding number is part of the next phrase.

The phrase tail broadly has three forms:

  1. single number: number
  2. conjoined pair: number abbr. number
  3. number list: number(, number)*( und number)?

An alternative, more permissive option for 3. would be to allow either commas or "und" as item separators in the list: number((,| und) number)*. Since this won't impact correctness of results and is more robust, this alternative form is used below.

Combining the branches, this gives the pattern: number( abbr. number|((,| und) number)*)?. As a regex, this is:

\d+( (Abs\.|Nr\.|Satz) \d+|((,| und) \d+)*)?

Note: this assumes there will always be exactly one space character between tokens. A more robust regex would replace all spaces with \s+; this should be done for a production regex, but it's not done in this answer for readability.

The regex for a full phrase is the phrase-start and phrase-tail regexes concatenated and then grouped:

(\d+ (Abs\.|Nr\.|Satz) \d+( (Abs\.|Nr\.|Satz) \d+|((,| und) \d+)*)?)

This contains capturing groups that aren't needed, so turn them into non-capturing groups:

(\d+ (?:Abs\.|Nr\.|Satz) \d+(?: (?:Abs\.|Nr\.|Satz) \d+|(?:(?:,| und) \d+)*)?)

Append the phrase separator pattern, and it's complete. In Python (also available on regex101):

import re

pattern = re.compile(r'(\d+ (?:Abs\.|Nr\.|Satz) \d+(?: (?:Abs\.|Nr\.|Satz) \d+|(?:(?:,| und) \d+)*)?)(?:, | und |$)');
text = "32 Abs. 8 und 10, 37 Abs. 5 Satz 3, 39 Abs. 3 und 40 Abs. 7, 8 und 10"
pattern.findall(text)
# result
['32 Abs. 8 und 10', '37 Abs. 5 Satz 3', '39 Abs. 3', '40 Abs. 7, 8 und 10']

How does this handle other cases? A few examples:

# another und
pattern.findall("39 Abs. 3 und 40 Abs. 7, 8 und 10 und 11 Nr. 12")
# result
['39 Abs. 3', '40 Abs. 7, 8 und 10', '11 Nr. 12']

# number list without ending "und"
pattern.findall("1 Abs. 2 und 3 Abs. 4, 5 und 6 Nr. 7")
# result
['1 Abs. 2', '3 Abs. 4, 5', '6 Nr. 7']

# und after und
pattern.findall("1 Abs. 2 und 3 Abs. 4, 5 Abs. 6 und 7 und 8 und 9 Nr. 10 und 11")
pattern.findall("1 Abs. 2 und 3 Abs. 4 und 5 Abs. 6 und 7 und 8 und 9 Nr. 10 und 11")
# result of both
['1 Abs. 2', '3 Abs. 4', '5 Abs. 6 und 7 und 8', '9 Nr. 10 und 11']

# all commas
pattern.findall("1 Abs. 2, 3 Abs. 4, 5 Abs. 6, 7, 8, 9 Nr. 10, 11")
# result
['1 Abs. 2', '3 Abs. 4', '5 Abs. 6, 7, 8', '9 Nr. 10, 11']

Suppose you wanted to know whether the pattern is complete. For it to be complete, it would need to fully match every string if the pattern were repeated (i.e. /^(pattern)*$/). In Python:

re.match(f"^({pattern.pattern})*$", text)

If you can find text for which the match fails, then the pattern is incomplete. Given the available sample data, there's no way to determine this.

Related