I'd like to propose a different approach: don't use a regex but check each character and collect password properties. That way you are able to implement more complex requirements later on, e.g. "it must have 3 out of 4".
Example:
String pw = "1a!cde";
Set<PwProperty> passwordProperties = new HashSet<>();
for( char c : pw.toCharArray() ) {
if( isDigit( c ) ) {
passwordProperties.add( PwProperty.DIGIT );
}
else if ( isSpecialChar( c ) ) {
passwordProperties.add( PwProperty.SPECIAL);
}
else if( isUpperCase( c ) ) {
passwordProperties.add( PwProperty.UPPER_CASE);
}
else if( isLowerCase( c ) ) {
passwordProperties.add( PwProperty.LOWER_CASE);
}
else {
passwordProperties.add( PwProperty.UNKNOWN );
}
}
Then you could check that like this (pseudo code):
if( !pw.length() in range ) {
display "password too short" or "password too long"
}
if( passwordProperties.contains( PwProperty.UNKNOWN ) {
display "unsupported characters used"
}
Set<PwProperty> setOf4 = { DIGIT, SPECIAL, LOWER_CASE, UPPER_CASE }
if( intersection( passwordProperties, setOf4 ).size() < 3 ) {
display "use at least 3 out of 4"
}
if( !passwordProperties.contains( DIGIT ) ) {
display "must contain digits"
}
display "password strength" being intersection( passwordProperties, setOfGoodProperties ).size()
etc.
This can then be expanded e.g. with properties like DIGIT_SEQUENCE which might be unwanted etc.
The main advantage is that you have more detailed information on the password rather than "it matches a certain regex or not" and you can use that information to guide the user.
(?![0-9]{5}$)a12345?a12345, see regex101.com/r/vY1vD7/1