2

I have the following string:

SEDCVBNT S800BG09 7GFHFGD6H 324235346 RHGF7U S8-00BG/09 7687678

and the following regex:

preg_match_all('/\b(?=.+[0-9])(?=.+[A-Z])[A-Z0-9-\/]{4,20}/i', $string, $matches)

What I'm trying to achieve is to return all of the whole "words" that:

  • contain at least 1 number
  • contain at least 1 letter
  • may contain /
  • may contain -

Unfortunately, the above regex returns purely alphabetical and purely numeric words as well:

Array (
  [0] => Array (
      [0] => SEDCVBNT
      [1] => S800BG09
      [2] => 7GFHFGD6H
      [3] => 324235346
      [4] => RHGF7U
      [5] => S8-00BG/09
  )
) 

I don't want SEDCVBNT or 324235346 to be returned.

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
Dave
  • 63
  • 1
  • 4

3 Answers3

2

You need slightly advanced regex syntax for this one.

The regex I came up with is

(?<=\s|^)(?=[\w/-]*\d[\w/-]*)(?=[\w/-]*[A-Za-z][\w/-]*)([\w/-])+(?=\s|$)

Let's explain it:

  • The syntax [\w/-] comes up a lot; this means "any word character (which includes letters, digits, accented letters etc) or a slash or a dash" -- effectively, all characters that you consider to be part of a valid token.
  • The regex uses positive lookahead to make sure that, at the place where a match is attempted, the following text does satisfy certain criteria. Positive lookahead looks like this: (?=[\w/-]*\d[\w/-]*).
  • It also uses positive (the one at the end: (?=\s|$)) and negative (at the beginning: (?<=\s|^)) lookahead to make sure that a match is only made if the whole text token begins after a whitespace character or is at the beginning of the input string (\s|^) and is followed by with a whitespace character or terminates the input string (\s|$).
  • Since the two interior lookahead patterns are almost identical to the capture group pattern ([\w/-])+, in effect I 'm using them to only match text that matches multiple patterns: both of the lookaheads and the capture group pattern at the end.
  • The first lookahead ensures that the next token includes at least one digit (\d).
  • The second lookahead ensures that the next token includes at least one letter (A-Za-z).
  • The capture group matches one or more word characters and/or / and -.

Therefore, for the capture group to match, the text being examined must:

  1. Be preceded either by whitespace or the beginning of the input string (this prevents partial-word matches starting after a disallowed character)
  2. Include at least one digit in the next stretch of allowed characters (first positive lookahead)
  3. Include at least one letter in the next stretch of allowed characters (second positive lookahead)
  4. Be comprised only of word characters, / and - (capturing group).
  5. Be followed either by whitespace or the end of the input string (this prevents partial-word matches ending at a disallowed character).

Which is exactly what you require. :)

Note: refiddle.com seems to not play well with negative lookbehind, so the regexp after the link does not include the initial (?<=\s|^) part. This means that it will erroneously match the DEF456 in ABC123$DEF456.

greg-449
  • 109,219
  • 232
  • 102
  • 145
Jon
  • 428,835
  • 81
  • 738
  • 806
  • @Dave: Replace the starting `/\b` (why is that `\b` in there?) with `~` and the ending `/i` (why case-insensitive? `A-Za-z` already covers that) with `~`. The problem is that you cannot use the delimiting character (which you get to pick, and you picked `/`) inside the regex (and we need to use `/` inside the regex). – Jon Jun 25 '11 at 15:28
  • I thought they all had to start and end with '/' :s (I haven't dealt with regex much). I've replaced it and it works! Thank you so much! – Dave Jun 25 '11 at 15:35
  • @Dave: The delimiters (starting character) are covered [here](http://www.php.net/manual/en/regexp.reference.delimiters.php). You might want to read some other bits from the manual as well. – Jon Jun 25 '11 at 15:38
  • @Dave: Updated the answer, now it won't match partial words. – Jon Jun 25 '11 at 17:30
  • `\w` incorrectly matches underscores which are not mentioned as whitelisted characters in the question. – mickmackusa May 04 '23 at 04:28
0

Word boundary markers (\b) cannot be relied upon for identifying the edges of a "word" for this task because, for one example, a word ending in slash followed by a space will not satisfy a word boundary. A word boundary is only appropriate when determining the zero-width position between a \w and \W (and vice versa).

Code: (Demo)

$string = 'SEDCVBNT S800BG09 7GFHFGD6H 324235346 RHGF7U S8-00BG/09 7687678';
preg_match_all(
    '~
      (?:^|\s)      #match start of string or whitespace
      \K            #release previously matched characters
      (?=\S*[a-z])  #lookahead for zero or more visible characters followed by letter
      (?=\S*\d)     #lookahead for zero or more visible characters followed by number
      [a-z\d/-]+    #match one or more consecutive whitelisted characters
      (?=\s|$)      #lookahead for a whitespace or the end of string
     ~xi',          #ignore literal whitespaces in pattern, use case-insensitivity with letters
    $string,
    $m
);
var_export($m);
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
-1

Here is the raw regex: \b(?=\S*?\d)(?=\S*?[a-z])\S+?(?=$|\s)

preg_match_all('/\b(?=\S*?\d)(?=\S*?[a-z])\S+?(?=$|\s)/i', $string, $matches) 
agent-j
  • 27,335
  • 5
  • 52
  • 79
  • @Dave: This regex will also match tokens like `abc123++$#%`, which you presumably do not want to match. Try to learn *why* and *how* something works (or does not), otherwise I don't see your hair lasting very long. – Jon Jun 25 '11 at 15:34
  • @Jon: Oh yeah you're right. I tried it with yours, and it returned 'abc123'. Is there a way to only return it if the whole token is a match? – Dave Jun 25 '11 at 15:41
  • @Dave, just replace the 3 instances of `\S` (capital S) with a `[\w-]` or whatever you want to match. – agent-j Jun 25 '11 at 17:23
  • This answer is provably incorrect for a number of reasons. https://3v4l.org/Y4bum – mickmackusa May 04 '23 at 04:23