zerosleeps

Since 2010

Advent of Code 2020 day 6

My Python solution for Advent of Code 2020 day 6. My initial working version was much more verbose than the copy below, but the tools used were the same - set and collections.Counter.

Very happy with my decision back during day 3 to split up puzzle input retrieval from parsing - it’s made testing the puzzle examples using the same code paths as testing my actual input much easier, which in turn makes me more likely to actually write those tests.

from collections import Counter
from pathlib import Path
import unittest

def get_raw_input():
    return (Path(__file__).parent / 'day_06_input.txt').read_text()

def parse_raw_input(raw_input):
    return [
            [
                [ char for char in person.strip() ]
            for person in group.splitlines() ]
        for group in raw_input.strip().split('\n\n')
    ]

class Group():
    def __init__(self, input):
        self.people = [person for person in input]

    @property
    def flattened_questions(self):
        return [question for person in self.people for question in person]

    @property
    def unique_questions(self):
        return set(self.flattened_questions)

    @property
    def questions_answered_by_all(self):
        counter = Counter(self.flattened_questions)
        return [question for question, count in counter.items() if count == len(self.people)]

def part_one(parsed_input):
    return sum([len(Group(group).unique_questions) for group in parsed_input])

def part_two(parsed_input):
    return sum([len(Group(group).questions_answered_by_all) for group in parsed_input])

if __name__ == '__main__':
    print(f'Part one: {part_one(parse_raw_input(get_raw_input()))}')
    print(f'Part two: {part_two(parse_raw_input(get_raw_input()))}')

class TestPartOneExamples(unittest.TestCase):
    def test_example_one(self):
        example = """abcx
            abcy
            abcz"""
        self.assertEqual(part_one(parse_raw_input(example)), 6)

    def test_example_two(self):
        example = """abc

            a
            b
            c

            ab
            ac

            a
            a
            a
            a

            b"""
        self.assertEqual(part_one(parse_raw_input(example)), 11)

class TestPartTwoExample(unittest.TestCase):
    def test_example(self):
        example = """abc

            a
            b
            c

            ab
            ac

            a
            a
            a
            a

            b"""
        self.assertEqual(part_two(parse_raw_input(example)), 6)

Advent of Code 2020 day 5

Back to Python today. I spent as long preparing the input as I did on anything else - for some reason I just can’t grok nested comprehensions in Python. Can write the equivalent nested loops no problem, but when it comes to re-factoring them…

Anyway here it is. Duplication in the row/column functions that could be better. And I’m sure there’s a cleverer way of grabbing multiple consecutive elements in an array for part two, like Ruby’s Enumerable#each_cons.

from pathlib import Path
import unittest

class BoardingPass():
    def __init__(self, input):
        self.input = input

    @property
    def row(self):
        candidates = list(range(128))
        for char in self.input[0:7]:
            if char == 'F':
                candidates = candidates[0:int(len(candidates) / 2)]
            else:
                candidates = candidates[int(len(candidates) / 2):]
        return candidates[0]

    @property
    def column(self):
        candidates = list(range(8))
        for char in self.input[-3:]:
            if char == 'L':
                candidates = candidates[0:int(len(candidates) / 2)]
            else:
                candidates = candidates[int(len(candidates) / 2):]
        return candidates[0]

    @property
    def seat_id(self):
        return ( self.row * 8 ) + self.column

def get_raw_input():
    return (Path(__file__).parent / 'day_05_input.txt').read_text()

def parse_raw_input(raw_input):
    return [ [ char for char in row.strip() ] for row in raw_input.strip().splitlines() ]

def part_one(raw_input):
    seat_ids = [ BoardingPass(parsed_input).seat_id for parsed_input in parse_raw_input(raw_input)]
    return max(seat_ids)

def part_two(raw_input):
    seat_ids = sorted(
        [ BoardingPass(parsed_input).seat_id for parsed_input in parse_raw_input(raw_input)]
    )

    for i, seat_id in enumerate(seat_ids):
        if seat_ids[i + 1] != seat_ids[i] + 1:
            return seat_ids[i] + 1

if __name__ == '__main__':
    print(part_one(get_raw_input()))
    print(part_two(get_raw_input()))

class TestPartOneExamples(unittest.TestCase):
    def test_example_one(self):
        bp = BoardingPass('FBFBBFFRLR')
        self.assertEqual(bp.row, 44)
        self.assertEqual(bp.column, 5)
        self.assertEqual(bp.seat_id, 357)

    def test_example_two(self):
        bp = BoardingPass('BFFFBBFRRR')
        self.assertEqual(bp.row, 70)
        self.assertEqual(bp.column, 7)
        self.assertEqual(bp.seat_id, 567)

    def test_example_three(self):
        bp = BoardingPass('FFFBBBFRRR')
        self.assertEqual(bp.row, 14)
        self.assertEqual(bp.column, 7)
        self.assertEqual(bp.seat_id, 119)

    def test_example_four(self):
        bp = BoardingPass('BBFFBBFRLL')
        self.assertEqual(bp.row, 102)
        self.assertEqual(bp.column, 4)
        self.assertEqual(bp.seat_id, 820)

Advent of Code 2020 day 4

My solution for Advent of Code 2020 day 4, this time in Ruby. I have a working Python solution as well, but it’s ugly: Ruby shines with chained methods and blocks.

I have a faint feeling that we’ll be revisiting this passport thing in future challenges…

class Passport
  def initialize(attributes)
    @attributes = attributes
  end

  def self.parse_raw_input(raw_input)
    raw_input
      .strip
      .split($/ + $/) # Passports are separated by two newlines in batch file
      .map do |passport|
        passport
          .gsub($/, ' ') # Each passport may be split into multiple lines
      end
      .map do |passport|
        passport
          .split # Conveniently, String.split also strips white-space
          .to_h do |element|
            [
              element.split(':').first.to_sym,
              element.split(':').last
            ]
          end
      end
  end

  def all_fields_present?
    [:byr, :iyr, :eyr, :hgt, :hcl, :ecl, :pid].all? do |f|
      @attributes.has_key?(f)
    end
  end

  def all_fields_valid?
    return false unless @attributes[:byr].to_i.between?(1920, 2002)
    return false unless @attributes[:iyr].to_i.between?(2010, 2020)
    return false unless @attributes[:eyr].to_i.between?(2020, 2030)
    return false unless (
      @attributes.has_key?(:hgt) && (
        @attributes[:hgt][-2..] == 'cm' && @attributes[:hgt][0..-3].to_i.between?(150, 193) ||
        @attributes[:hgt][-2..] == 'in' && @attributes[:hgt][0..-3].to_i.between?(59, 76)
      )
    )
    return false unless @attributes[:hcl] =~ /^#[\da-f]{6}$/
    return false unless ['amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth'].count(@attributes[:ecl]) == 1
    return false unless @attributes[:pid] =~ /^\d{9}$/
    return true
  end

end

def part_one(raw_input)
  passports = Passport.parse_raw_input(raw_input).map do |parsed_input|
    Passport.new(parsed_input)
  end
  passports.filter { |p| p.all_fields_present? }.length
end

def part_two(raw_input)
  passports = Passport.parse_raw_input(raw_input).map do |parsed_input|
    Passport.new(parsed_input)
  end
  passports.filter { |p| p.all_fields_valid? }.length
end

puts "Part one: #{part_one(IO.read(File.join(__dir__, '../day_04_input.txt')))}"
puts "Part two: #{part_two(IO.read(File.join(__dir__, '../day_04_input.txt')))}"

Advent of Code 2020 day 3

My Python solution for Advent of Code 2020 day 3. Nested lists and row/column coordinates always make my brain hurt.

I took a gamble for part one and decided not to build the repeating pattern when needed, instead using the mod/wraparound approach. Got lucky that part two didn’t build on this component of the puzzle!

Grid could do with a little love, e.g. why does it have an ‘x’ getter but not a ‘y’ getter?

Ooh one other change I made is to the way I parse the puzzle input. In past years (and in this years previous challenges) I’ve usually had a get_input function that reads the input file and parses it. That has meant that my unit tests - with their hardcoded “example” inputs - have had to use pre-parsed inputs (or I’ve had to parse the example inputs manually before sticking it in the unit test).

I changed that today so that the Grid objects themselves parse raw input, and I can therefore feed them either text files or sample strings, which in turn means the parsing itself is now getting tested. Much better - will continue doing that.

from math import prod
from pathlib import Path
import unittest

def get_input():
    return (Path(__file__).parent / 'day_03_input.txt').read_text()

class Grid():
    def __init__(self, input):
        self.grid = self.parse_input(input)
        self.position = {'x': 0, 'y': 0}

    @classmethod
    def parse_input(self, input):
        return [ [ char for char in line.strip() ] for line in input.splitlines() ]

    @property
    def grid_height(self):
        return len(self.grid)

    @property
    def grid_width(self):
        """Return width of slope

        Assumes all rows have the same width
        """
        return len(self.grid[0])

    @property
    def x(self):
        """Return effective x position

        Accounts for arboreal genetics and biome stability
        """
        return self.position['x'] % self.grid_width

    def past_bottom(self):
        if self.position['y'] > ( self.grid_height - 1 ):
            return True
        else:
            return False

    def slope(self, delta_x, delta_y):
        obstacles = []
        while not self.past_bottom():
            obstacles.append(self.grid[self.position['y']][self.x])
            self.position['y'] += delta_y
            self.position['x'] += delta_x
        return obstacles

def part_one(input):
    return Grid(input).slope(3,1).count('#')

def part_two(input):
    slope_results = []
    slopes_to_check = [[1,1],[3,1],[5,1],[7,1],[1,2]]
    for slope in slopes_to_check:
        grid = Grid(input)
        slope_results.append(grid.slope(slope[0], slope[1]).count('#'))
    return prod(slope_results)

class TestExamples(unittest.TestCase):
    def setUp(self):
        self.example_grid = """..##.......
            #...#...#..
            .#....#..#.
            ..#.#...#.#
            .#...##..#.
            ..#.##.....
            .#.#.#....#
            .#........#
            #.##...#...
            #...##....#
            .#..#...#.#"""

    def test_part_one(self):
        self.assertEqual(part_one(self.example_grid), 7)

    def test_part_two(self):
        self.assertEqual(part_two(self.example_grid), 336)

if __name__ == '__main__':
    print(f'Part one: {part_one(get_input())}')
    print(f'Part two: {part_two(get_input())}')

Advent of Code 2020 day 2

My Python solution for Advent of Code 2020 day 2. Taking the object-orientated path when I tackled part one didn’t really buy me much for part two, but I stand by my decision!

from pathlib import Path
import re
import unittest

def get_input():
    input_file = Path(__file__).parent / 'day_02_input.txt'

    with open(input_file) as file:
        return [line.strip() for line in file.readlines()]

class Password():
    def __init__(self, input):
        match = re.match('^(\d+)-(\d+) (\w): (\w+)$', input)
        self.min = int(match[1])
        self.max = int(match[2])
        self.letter = match[3]
        self.password = match[4]

    def valid_password(self):
        c = self.password.count(self.letter)
        if c >= self.min and c <= self.max:
            return True
        else:
            return False

    def new_valid_password(self):
        if (self.password[self.min - 1] == self.letter) ^ (self.password[self.max - 1] == self.letter):
            return True
        else:
            return False

class TestExamples(unittest.TestCase):
    def setUp(self):
        example_list = """1-3 a: abcde
                   1-3 b: cdefg
                   2-9 c: ccccccccc"""
        self.passwords = [Password(p.strip()) for p in example_list.splitlines()]

    def test_part_one(self):
        self.assertEqual(len([p for p in self.passwords if p.valid_password()]), 2)

    def test_part_two(self):
        self.assertEqual(len([p for p in self.passwords if p.new_valid_password()]), 1)

if __name__ == '__main__':
    passwords = [Password(p) for p in get_input()]
    print(f'Part one: {len([p for p in passwords if p.valid_password()])}')
    print(f'Part two: {len([p for p in passwords if p.new_valid_password()])}')