Selecting all Permutations Without Repetition using Regular Expressions in Python

Viewed 127

I have three classes of characters, say letters [A-Za-z], numbers [0-9], and symbols [!@#$]. The particular symbols don't really matter for the sake of the argument. I would like to use a regular expression in Python so that I can select all permutations on these three classes with a length of three, without repetition.

For example, the following would successfully match:

a1!
4B_
*x7

And the following would fail:

ab!
BBB
*x_
a1!B

How would I go about this without explicitly writing out each potential permutation of classes in my regular expression?

I have previously attempted the following solution:

import re
regex = r"""
              ([A-Za-z]|[0-9]|[!@#$])
    (?!\1)    ([A-Za-z]|[0-9]|[!@#$])
    (?![\1\2])([A-Za-z]|[0-9]|[!@#$])
    """
s = "ab1"
re.fullmatch(regex, s, re.VERBOSE)

However the string ab1 is improperly matched. This is because the group references \1 and \2 refer to the actual matched contents of the groups, and not to the regular expressions contained within the groups.

Then, how do I refer to the regular expressions contained within prior matched groups, and not to their actual contents?

2 Answers

Your main problem is that you can't use backreferences to negate a part of a pattern, you may only use them to match/negate the same values as captured in the corresponding capturing group.

Note [^\1] matches any char other than the \x01 char, not any char other than what Group 1 holds, because, inside character classes, backreferences are no longer reated as such. ab1 is matched becauie b is not equal to a and 1 is not equal to a and 1.

What you can use is a sequence of negative lookaheads that would "exclude" matching under certain conditions, like the string cannot have two digits/letters/special chars.

rx = re.compile(r"""
  (?!(?:[\W\d_]*[^\W\d_]){2})      # no two letters allowed
  (?!(?:\D*\d){2})                 # no two digits allowed
  (?!(?:[^_!@\#$*]*[_!@\#$*]){2})  # no two special chars allowed
  [\w!@\#$*]{3}                    # three allowed chars
""", re.ASCII | re.VERBOSE)

See the regex demo. The negated character classes are replaced with .* in the demo since the test is performed against a single multiline text and not separate strings.

See the Python demo:

import re
passes = ['a1!','4B_','*x7']
fails = ['ab!','BBB','*x_','a1!B']
rx = re.compile(r"""
  (?!(?:[\W\d_]*[^\W\d_]){2})      # no two letters allowed
  (?!(?:\D*\d){2})                 # no two digits allowed
  (?!(?:[^_!@\#$*]*[_!@\#$*]){2})  # no two special chars allowed
  [\w!@\#$*]{3}                    # three allowed chars
""", re.ASCII | re.VERBOSE)
for s in passes:
    print(s, ' should pass, result:', bool(rx.fullmatch(s)))
for s in fails:
    print(s, ' should fail, reuslt:', bool(rx.fullmatch(s)))

Output:

a1!  should pass, result: True
4B_  should pass, result: True
*x7  should pass, result: True
ab!  should fail, reuslt: False
BBB  should fail, reuslt: False
*x_  should fail, reuslt: False
a1!B  should fail, reuslt: False

A straightforward solution is to not write out the permutations yourself, but to let Python do it with the help of itertools.

from itertools import permutations

patterns = [
    '[a-zA-Z]',
    '[0-9]',
    '[!@#$]'
]

regex = '|'.join(
    ''.join(p)
    for p in permutations(patterns)
)
Related