Skip to main content
5 of 5
Renamed functions, used any() (I was actually trying to keep it as simple as possible, but sure, why not...), fixed up some erroneous logic
Yuushi
  • 11.1k
  • 2
  • 31
  • 66

Code length is one factor in readability, but it isn't the only factor. "Is this code easy to read and modify" is a more important question to ask.

It looks like you've gotten a method skeleton here, which kind of leads you down the path of stuffing everything in one function, which is a bit of a shame. Let's look at what we actually need here.

  1. Length of at least 6
  2. One lowercase English character
  3. One uppercase English character
  4. One digit
  5. One special character

What if we split this up into a bunch of separate functions, which each do one thing. We'll just write the function names down for now, and leave the implementation blank.

def lower_required(text):
    pass

def upper_required(text):
    pass

def digits_required(text):
    pass

def length_required(text):
    pass

def special_required(text):
    pass

Now, Python has a string module that is (semi)deprecated (much of what was in it is now on string objects themselves), but that has some still useful things, namely, string.ascii_lowercase, string.ascii_uppercase, and string.digit. We can use these in the implementations for some of our functions. Let's just write them as simply as possible for now.

import string

def lower_required(text):
    for char in text:
        if char in string.ascii_lowercase:
            return 0
    return 1

def upper_required(text):
    for char in text:
        if char in string.ascii_uppercase:
            return 0
    return 1

def digit_required(text):
    for char in text:
        if char in string.digits:
            return 0
    return 1

There's quite a bit of repetition here that we can factor out.

def num_required(text, characters):
    if any((x in characters for x in text)):
        return 0
    return 1

Then our previous functions can just call this:

def lower_required(text):
    return num_required(text, string.ascii_lowercase)

And similarly for the upper_required and digit_required.

In fact, we can do this with our special characters as well:

SPECIAL_CHARS = '!@#$%^&*()-+'

def special_required(text):
    return num_required(text, SPECIAL_CHARS)

The last function we need to implement is length_required:

MINIMUM_LENGTH = 6

def length_required(text):
    if len(text) >= MINIMUM_LENGTH:
        return 0
    return MINIMUM_LENGTH - len(text)

Now we can stitch these all together within minimumNumber (note that Python functions should be named using snake_case, so it should be minimum_number here - but I assume this function name was given to you, so I won't harp on about it too much).

def minimumNumber(text):
    test_functions = [lower_required, upper_required,
                      digits_required, special_required]
    min_chars_required = sum([test_fn(text) for test_fn in test_functions])
    missing_length = length_required(text)
    if missing_length > min_chars_required:
        return missing_length
    return min_chars_required

The benefit of doing this is the fact that we can easily add extra checks. What if a new requirement comes along? We can just create a new function an add it to test_functions; the amount of existing code we have to touch is minimal.

Just to drive home the point about size vs readability, here is a (slightly) code golfed version:

import string

def minimumRequired(password):
    if len(password) < 6:
        return 6 - len(password)
    unique_pw = frozenset(password)
    char_sets = (frozenset(string.ascii_lowercase), frozenset(string.ascii_uppercase),
                 frozenset(string.digits), frozenset('!@#$%^&*()-+'))
    required_extra = 0
    for char_set in char_sets:
        if not char_set & unique_pw:
            required_extra += 1
    return required_extra

This (probably - I haven't really tested it) satisfies the requirements, but how do I make changes to it? Is it obvious how it does what it does (I'd argue not really).

Yuushi
  • 11.1k
  • 2
  • 31
  • 66