Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The best way to match at least three out of four regex requirements

In password strategy, there are 4 requirements. It should contains any three of the following

  1. lower case.
  2. upper case.
  3. numeric.
  4. special character.

The following regex will match all cases

^(?=.*\d)(?=.*[a-z])(?=.*[A-Z])(?=.*[^a-zA-Z0-9]).{4,8}$

I know I can use '|' to declare all combinations, however, that will produce a supper long regex. What is the best way to replace '|' so that it can check if the input contains any of three conditions in the combination?

like image 846
panda Avatar asked Nov 18 '14 10:11

panda


People also ask

How to match regex?

To match a character having special meaning in regex, you need to use a escape sequence prefix with a backslash ( \ ). E.g., \. matches "." ; regex \+ matches "+" ; and regex \( matches "(" . You also need to use regex \\ to match "\" (back-slash).

What is?= in regex?

(?= regex_here) is a positive lookahead. It is a zero-width assertion, meaning that it matches a location that is followed by the regex contained within (?=

What is regex used for?

Short for regular expression, a regex is a string of text that lets you create patterns that help match, locate, and manage text. Perl is a great example of a programming language that utilizes regular expressions.


2 Answers

If you're using a PCRE flavor, the following one could suit your needs (formatted for readability):

^(?:
    ((?=.*\d))((?=.*[a-z]))((?=.*[A-Z]))((?=.*[^a-zA-Z0-9]))|
    (?1)      (?2)         (?3)                             |
    (?1)      (?2)                      (?4)                |
    (?1)                   (?3)         (?4)                |
              (?2)         (?3)         (?4)
).{4,8}$

Regular expression visualization

One-lined:

^(?:((?=.*\d))((?=.*[a-z]))((?=.*[A-Z]))((?=.*[^a-zA-Z0-9]))|(?1)(?2)(?3)|(?1)(?2)(?4)|(?1)(?3)(?4)|(?2)(?3)(?4)).{4,8}$

Demo on Debuggex


JavaScript regex flavor does not support recursion (it does not support many things actually). Better use 4 different regexes instead, for example:

var validate = function(input) {
    var regexes = [
        "[A-Z]",
        "[a-z]",
        "[0-9]",
        "[^a-zA-Z0-9]"
    ];    
    var count = 0;
    for (var i = 0, n = regexes.length; i < n; i++) {
        if (input.match(regexes[i])) {
            count++;
        }
    }    
    return count >=3 && input.match("^.{4,8}$");    
};
like image 128
sp00m Avatar answered Oct 25 '22 08:10

sp00m


Sure, here's a method which uses a slight modification of the same regex, but with a short bit of code authoring required.

^(?=(\D*\d)|)(?=([^a-z]*[a-z])|)(?=([^A-Z]*[A-Z])|)(?=([a-zA-Z0-9]*[^a-zA-Z0-9])|).{4,8}$

Here you have five capturing groups - Check whether at least 3 of them are not null. A null group for capture effectively indicates that the alternative within the lookahead has been matched, and the capturing group on the left hand side could not be matched.

For example, in PHP:

preg_match("/^(?=(\\D*\\d)|)(?=([^a-z]*[a-z])|)(?=([^A-Z]*[A-Z])|)(?=([a-zA-Z0-9]*[^a-zA-Z0-9])|).{4,8}$/", $str, $matches);

$count = -1; // Because the zero-eth element is never null.
foreach ($matches as $element) {
    if ( !empty($element)) {
        $count += 1;
    }
}

if ($count >= 3) {
    // ...
}

Or Java:

Matcher matcher = Pattern.compile(...).matcher(string);

int count = 0;

if (matcher.matches())
    for (int i = 1; i < 5; i++)
        if (null != matcher.group(i))
            count++;

if (count >= 3)
    // ...
like image 35
Unihedron Avatar answered Oct 25 '22 08:10

Unihedron