Reputation: 3
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
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
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