Advent of Code 2020, Day 2 -- Password Philosophy

December 2nd, 2020

My Advent of Code solutions, done in Python. Refactors and solutions in other languages will be added if/when they're done.

Full code for Day 2 can be found on Github.

You can participate in Advent of Code at adventofcode.com/2020

The Day 2 problem can be found at adventofcode.com/2020/day/2

Index

Solution, part 1

My approach for this problem was to split each line in the puzzle input into each component part: the minimum number of instances of a letter in the password, the maximum number of instances, the letter being searched for, and the password string.

In this case, I used .split() to split the initial string by spaces, then used it again to split the number min/max. Once I had that, I used .count() to count the number of times the letter appeared in the password, then compared that to the min and max. If it's true, it increments the valid_passwords variable by 1, then returns that amount when the list has been completed.

puzzle_input = open("input.txt", "r")
lines = puzzle_input.readlines()
lines = [line.strip() for line in lines]


# Splits each line into component parts:
# min/max bounds, target letter, and password.
# Counts the number of instances of the letter in the password.
# Increments the # of valid passwords if instances
# is between max and min.
def part_1(passwords):
    valid_passwords = 0
    for p in passwords:
        sp = p.split(" ")
        policy = sp[0].split("-")
        min = int(policy[0])
        max = int(policy[1])
        letter = sp[1][0]
        password = sp[2]
        instances = password.count(letter)
        if max >= instances >= min:
            valid_passwords += 1
    return valid_passwords


print(part_1(lines))

Solution, part 2

The solution for this part of the problem is almost exactly the same as part 1, except instead of checking for the count of the letters in the password string, we're checking the letters at the indicated indices. I've split up the string in the same way, but instead of calling the numbers "min" and "max", I'm calling them "idx1" and "idx2". I'm also subtracting 1 from each index, because "Toboggan Corporate Policies have no concept of 'index zero'!".

To check the password, I'm using XOR to see if either character in the password is true, but not both. In this case, I'm using the bitwise XOR, which doesn't really seem right, but it ended up working out for me. (My knowledge of bitwise operations is very little.)

# Splits each line into component parts:
# indices, target letter, and password.
# Checks to see if the target letter is at idx1 XOR idx2.
# Using XOR here because we only want to count if it occurs once.
def part_2(passwords):
    valid_passwords = 0
    for p in passwords:
        sp = p.split(" ")
        policy = sp[0].split("-")
        idx1 = int(policy[0]) - 1
        idx2 = int(policy[1]) - 1
        letter = sp[1][0]
        password = sp[2]
        if (password[idx1] == letter) ^ (password[idx2] == letter):
            valid_passwords += 1
    return valid_passwords


print(part_2(lines))

Refactoring both parts of the problem

I did a little bit of tinkering after completing the puzzle and thought, wouldn't it be interesting if I:

  1. Used regex to parse the string instead of splitting the string?
  2. Made the function able to complete both parts of the puzzle?

I've never used named groups in Python before, in fact, I don't know if I've used regex in Python before, so this was an interesting thing to learn. A named group in Python regex is created with the syntax (?P<name>...) with the regex where the "..." is. Then, the group can be accessed with .group(name).

My regex is very long and could probably be shortened in some way by a regex master, but it gets the job done.

Originally I wanted to use the group names instead of assigning them to variables, but it got so unwieldy to continually call .group() that I eventually gave up and assigned each group to a variable.

I'm also passing a "method" argument into the function that will either count the number of letters or check the indices. I tried to use a switch statement but didn't realize that Python doesn't have switch statements, so an if statement it is.

I've also added a bit of error checking for a case where the provided indices were higher than the length of the password, and if the method isn't passed in the function call.

Personally, I think this refactor is worse than the original solutions, but it was worth it to learn something new about Python regex!

# Attempt with regex.
# It's actually messier than the original.
# But I've never used named groups before.
# Also attempted to use one function for both methods,
# which makes things messier as well.
def password_checker(passwords, method):
    reg = re.compile('(?P<num1>\d+)-(?P<num2>\d+)(\s)(?P<letter>\w)(:\s)(?P<password>\w+)')
    valid_passwords = 0
    for p in passwords:
        m = reg.match(p)
        password = m.group('password')
        letter = m.group('letter')
        n1 = int(m.group('num1'))
        n2 = int(m.group('num2'))
        if method == "part_1":
            instances = password.count(letter)
            if n1 <= instances <= n2:
                valid_passwords += 1
        elif method == "part_2":
            if len(password) < n2:
                return "The index is too high!"
            if (password[n1 - 1] == letter) != (password[n2 - 1] == letter):
                valid_passwords += 1
        else:
            return "Select a valid method: part_1 or part_2"
    return valid_passwords
    

print(password_checker(lines, "part_1"))
print(password_checker(lines, "part_2"))

More Advent of Code


If you liked this post, why not follow me on Twitter for more of my nonsense? Or follow my projects on Github.