How to make a pattern match 4 digits with at least one letter?

Viewed 148

I have the following solution:

str = "0e4f9a4fdcfb351397b65cdcabcd0673d768cf59"    
patten = re.compile("\d(?=.*[a-z])[a-z\d]{4}\d")

searchObj = patten.findall(commit)

searchObj
['351397', '0673d7', '68cf59']

here (?=.*[a-z]) means "at least one letter".

but the result includes 351397, there is no letter at all.

Why did it get only numbers without the letter?

2 Answers

As noted in the comments, the expression matches 6 digits because somewhere later on there's a letter in the string. The .*[a-z] part of the lookahead will take every character past it's present location and if it finds any character that's a letter, it reports "true" and the engine continues with [a-z\d]{4}\d which will match digits only too.

To ensure that at least one letter is present for a 6 character pattern that is:

  • a digit
  • 4 characters that are digits or letters, but at least one of them is a letter
  • a digit

you can make the engine go to the middle of the 6 characters and check to see if any of the past 2 was a letter or if any of the next two is a letter. It's not the most beautiful solution, but it seems to work:

\d[a-z\d]{2}(?:(?<=[a-z].|.[a-z])|(?=[a-z].|.[a-z]))[a-z\d]{2}\d

Breaking it down:

\d                   # a digit
[a-z\d]{2}           # 2 characters
(?:                  # non-capturing group
    (?<=             # look behind
          [a-z].     # a letter and something
        |            # or
          .[a-z]     # something and a letter
    )
  |                  # or
    (?=              # look ahead
          [a-z].     # a letter and something
        |            # or
          .[a-z]     # something and a letter
    )
)
[a-z\d]{2}           # 2 characters
\d                   # a digit

regex101


The solution by JvdV in a comment saying

Maybe \d(?!\d{5})[a-z\d]{4}\d is more what you are after.

is, in my opinion, much more elegant, because after it takes the first digit, it checks to see that the next 5 characters are not 5 digits in a row. It will also work with checking that the next 4 characters are not only digits using the negative lookahead (?!\d{4}) instead.

Use

\d(?=\d{0,3}[a-z])[a-z\d]{4}\d

See regex proof.

EXPLANATION

--------------------------------------------------------------------------------
  \d                       digits (0-9)
--------------------------------------------------------------------------------
  (?=                      look ahead to see if there is:
--------------------------------------------------------------------------------
    \d{0,3}                  digits (0-9) (between 0 and 3 times
                             (matching the most amount possible))
--------------------------------------------------------------------------------
    [a-z]                    any character of: 'a' to 'z'
--------------------------------------------------------------------------------
  )                        end of look-ahead
--------------------------------------------------------------------------------
  [a-z\d]{4}               any character of: 'a' to 'z', digits (0-9)
                           (4 times)
--------------------------------------------------------------------------------
  \d                       digits (0-9)
Related