RocketShip
RocketShip

Reputation: 3

Deem password invalid if it contains N number of consecutive ascending alphanumeric characters

I have a specification to not allow users to create passwords with a certain number of incremented letters or numbers such as: abc, xyz, hij, 678, 789, or RST.

I have found regex match patterns which will match aaa or 777, but not ascending sequences.

How can I match these kinds of sequences?

Upvotes: -5

Views: 313

Answers (2)

mickmackusa
mickmackusa

Reputation: 48031

One approach is to step through each character of the string and keep track of the number of consecutive incremented alphanumeric characters. Returning early from a single loop will ensure the best possible performance.

Code: (Demo from PHP8.3 and up) (Demo if you are using PHP5.4 through PHP8.2)

function hasTooManyAscChars($str, $limit = 3) {
    if ($limit === 1) {
        throw new Exception('Using 1 as $limit parameter will cause failure on any alphanumermic character.');
    }
    $i = 1;
    $prev = null;
    foreach (mb_str_split($str) as $char) {
        if (ctype_alnum((string) $prev) && $char === str_increment($prev)) {
            ++$i;
            if ($i === $limit) {
                return true;
            }
        } else {
            $i = 1;
        }
        $prev = $char;
    }
    return false;
}

Another approach (which can technically be written as a one-liner) is using regex to only isolate substrings comprised of numeric, lowercase, or uppercase characters with a length equal to the declared limit. If there are no qualifying substrings, then nothing more needs to be done -- the password passed this validation rule. To be clear, the lookahead allows, say, character s 1 through 4 to be isolated, then 2 through 5, etc. (if they qualify).

If there are matches, iterate over them to determine if they are ascendingly sorted and have no gaps. In array_reduce(), I use the bitwise OR operator (|) to ensure that as soon as a true evaluation is encountered, no more processing function calls are make -- this is the best optimization of performance while an earlier return is *not possible (by not possible -- I mean that I'd have to throw an exception in a rather inelegant fashion to break the iterations within array_reduce()).

Code: (Demo) (Demo for outdated versions down to PHP5.5)

function hasTooManyAscChars($str, $limit = 3) {
    if ($limit === 1) {
        throw new Exception('Using 1 as $limit parameter will cause failure on any alphanumermic character.');
    }
    return preg_match_all(
            "/(?=(?|(\d{{$limit}})|([a-z]{{$limit}})|([A-Z]{{$limit}})))/u", #capture N digits, N lowers, or N uppers
            $test,
            $m,
            PREG_SET_ORDER
        )
        &&                                                                   #only continue processing if matches are made
        array_reduce(
            array_column($m, 1),
            fn($result, $chars) => $result
                |                                                            #if $result is true, don't process rightside
                (
                    count_chars($chars, 3) === $chars                        #mode 3 will return unique chars in asc order
                    &&
                    count(range($chars[0], $chars[$limit - 1])) === $limit   #number of elements in range equals limit
                ),
            false
        );
}

Test input variables and function execution script:

$tests = [
    'Asdfghjk',
    '135792456790',
    'cremnophobia',
    '8675309',
    'analyzable',
    'UNDERSTUDY',
    'EasyAsABC1234',
    'Çüéâäàå',
    "\u{1f600}\u{1f601}\u{1f602}\u{1f603}\u{1f604}",
];
$limit = 4;

foreach ($tests as $test) {
    printf(
        "%15s: %s\n",
        $test,
        hasTooManyAscChars($test, $limit) ? 'Fail' : 'Pass'
    );
}

Output from either approach using the same test procedure:

       Asdfghjk: Pass
   135792456790: Fail
   cremnophobia: Fail
        8675309: Pass
     analyzable: Pass
     UNDERSTUDY: Fail
  EasyAsABC1234: Fail
 Çüéâäàå: Pass
😀😁😂😃😄: Pass

Upvotes: 0

Sujit Baniya
Sujit Baniya

Reputation: 915

You could use something like this:

function inSequence($string, $num = 4) {
    $i = 0;
    $j = strlen($string);
    $str = implode('', array_map(function($m) use (&$i, &$j) {
        return chr((ord($m[0]) + $j--) % 256) . chr((ord($m[0]) + $i++) % 256);
    }, str_split($string, 1)));
    return preg_match('#(.)(.\1){' . ($num - 1) . '}#', $str);
}

Upvotes: 0

Related Questions