Tackling Advent of Code 2020 in emacs lisp

Resources

Advent of code

Utility functions

Read data

Utility functions for loading the input data. read-lines adapted from an example by Xah Lee.

	  (defun read-lines (filePath)
	    "Return a list of lines of a file at filePath."
	    (with-temp-buffer
	      (insert-file-contents filePath)
	      (split-string (buffer-string) "\n" nil)))

	  (defun line-to-numbers (line)
	    "Convert a whitespace separated list of strings into numbers"
	    (apply 'string-to-number
		    (split-string line))
	    )

	  (defun read-numbers (filePath)
	    "Return a list of all the numbers in a file at filePath."
	    (let (
		  (lines (read-lines filePath))
		  )
		 (mapcar 'line-to-numbers lines))
	    )

Day 01

Instructions

Part 1

Requirements

  1. Load puzzle input
  2. Find which 2 values in puzzle input sum to 2020
  3. Return product of those 2 values

Load data

(setq data (read-numbers '"inputs/day1.txt"))

Processing

Loop over each value, and return the values where 2020 - value is in the list of all values. Return those two values (since we’ll find both halves in a pass through the full list).

(defun inverse-in-list (sequence item) (if (member (- 2020 item) sequence) t nil))
(setq result (seq-filter (apply-partially 'inverse-in-list data) data))

And the final result is the product of those two values:

(setq result_part1 (eval (cons '* result)))

I think this approach of inserting the operator into the start of the list, and then evaluating the list, is the “lisp-ish” way to multiply the list together. But I’m not certain there isn’t a better method.

Part 2

Requirements

  1. Find product of the three numbers that sum to 2020

Processing

Already have processing for 2 numbers that sum to 2020. Can we loop over each value in the input data and calculate if two numbers in the input sum to (2020 - current loop value)?

First, can we change inverse-in-list to use an arbitrary value rather than 2020?

(defun inverse-in-list (sequence item reference) (if (member (- item reference) sequence) t nil))
(setq result (seq-filter
		(apply-partially 'inverse-in-list data 2020) data))

And now loop over each value in the input, and call the new inverse-in-list with a reference of 2020 - value:

(setq result '())
(dolist (current-data data result)
  (setq interim_result (seq-filter
		  (apply-partially 'inverse-in-list data (- 2020 current-data)) data))
  (if interim_result (push current-data result))
)

In English, the above creates a result list to accumulate the values we want to multiply. Then, for each current-data in the data list, we check if any two values in the list add up to (2020 - current-data). If they do, we accumulate the current-data. Notice that the third argument of do-list is the returned value, in this case the accumulated result.

Finally, a similar multiplication as for part 1:

(setq result_part2 (eval (cons '* result)))

Day 02

Instructions

Part 1

Requirements

  1. Load puzzle input
  2. Count valid passwords in input, where valid password defined as:

    suppose you have the following list:

    1-3 a: abcde 1-3 b: cdefg 2-9 c: ccccccccc Each line gives the password policy and then the password. The password policy indicates the lowest and highest number of times a given letter must appear for the password to be valid. For example, 1-3 a means that the password must contain a at least 1 time and at most 3 times.

Processing

So let’s start with a function to validate a single line:

(defun password-break-line (line)
  "Breaks a line from the day 2 input in to the parts needed for validation"
  (let* (
	   (parts (split-string line))
	   (counts (split-string (elt parts 0) "-")))
  (setq
	   min-count (string-to-number (elt counts 0))
	   max-count (string-to-number (elt counts 1))
	   letter (substring (elt parts 1) 0 1)
	   password (elt parts 2)
	   )
    )
)

(defun validate-password (line)
  "Checks if a password is valid according to the day 2 instructions."
  (message line)
  (password-break-line line)
  (let (
	  (letter-count
	   (- (length
	       (split-string password
			     letter
			     )
	       )
	      1)
	   )
	  )
    (setq result (<= min-count letter-count max-count))
  )
  result
  )
(validate-password "2-9 c: ccccccccc")

And now apply across all lines:

(setq data (read-lines '"inputs/day2.txt"))
(setq result (seq-filter 'validate-password data))
(length result)

Which is the right result for part 1 of day 2.

Part 2

Requirements

  1. Load puzzle input
  2. Count valid passwords in input, where valid password defined as (note different to part 1):

    Each policy actually describes two positions in the password, where 1 means the first character, 2 means the second character, and so on. (Be careful; Toboggan Corporate Policies have no concept of “index zero”!) Exactly one of these positions must contain the given letter. Other occurrences of the letter are irrelevant for the purposes of policy enforcement.

    Given the same example list from above:

    1-3 a: abcde is valid: position 1 contains a and position 3 does not. 1-3 b: cdefg is invalid: neither position 1 nor position 3 contains b. 2-9 c: ccccccccc is invalid: both position 2 and position 9 contain c.

Processing

Need a different validation:

 (defun get-letter (line index))
   "Returns letter from line at (1-indexed) index."
   (let ((result (aref 'line (1+ 'index))))))

   (defun validate-password (line)
     "Checks if a password is valid according to the day 2 instructions."
     (message line)
     (password-break-line line)
     (let (
	    (letter-count
	     (- (length
		 (split-string password
			       letter
			       )
		 )
		1)
	     )
	    )
	(setq result (<= min-count letter-count max-count))
     )
     result
     )

and test cases:

(let ((test-result (list 
		   (validate-password "1-3 b: cdefg")
		   (validate-password "2-9 c: ccccccccc"))))
  (equal 'test-result (list t nil))
)

Day 04

Note that if you’re using the github viewer for this file, the results are all hidden. Use the ‘raw’ button to see results (or download and view in the one true editor).

Part 1

Skipping to day 4 for a python experiment. First, load the day 4 data with lisp. Loading the data isn’t an interesting python problem, but combining lisp and python is an interesting problem:

(setq day4-data (read-lines '"inputs/day4.txt"))

Now to use the data loaded with lisp in python. First, we’ll set up a function to convert the data into a list of dicts:

 def generate_passports(data):
   """
   Converts data into a list of dicts.  

   Each dict represents a single passport.  Passports are delineated in the
   data by a blank line.

   """
   passports = []
   passport = {}
   for line in data:
     if line == "":
	passports.append(passport)
	passport = {}
	continue
     for pairs in line.split():
	items = pairs.split(':')
	try:
	  passport[items[0]] = items[1]
	except IndexError:
	  print(f"Parse error: {items}")
   passports.append(passport)  # Need to include last passport since input
				# doesn't have a blank line at the end
   return passports

A quick test that generate_passports works roughly as expected:

data = ["ecl:gry pid:860033327 eyr:2020 hcl:#fffffd",
"byr:1937 iyr:2017 cid:147 hgt:183cm",
"",
"iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884",
"hcl:#cfa07d byr:1929",]
test_result = generate_passports(data)

import pprint
pprint.pprint(test_result)

Now to call generate_passports with the lisp loaded data. Notice that the data variable being passed into the python session is defined as the name of the lisp code block, not the variable being set in the code block (with the implication that running the python block also re-runs the lisp block):

passports = generate_passports(data)

Now we need to validate the passports:

def is_valid_passport(passport):
  """Returns True for valid passport, False otherwise.

  Valid passport defined as including the following keys:
  byr (Birth Year)
  iyr (Issue Year)
  eyr (Expiration Year)
  hgt (Height)
  hcl (Hair Color)
  ecl (Eye Color)
  pid (Passport ID)

  """
  required_keys = ("byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid")
  result = True
  for key in required_keys:
     if key not in passport.keys():
        result = False
  return result

Verify with the test data. From instructions, the first test data passport is valid while the second is not:

[is_valid_passport(passport) for passport in test_result]

Now run on full set of passports. Note we only want the total number of valid passports:

len([passport for passport in passports if is_valid_passport(passport)])

Which is the right answer for part 1.

Part 2

This has a different validation:

import re

def is_in_range(number, minimum, maximum):
  try:
      result = minimum <= int(number) <= maximum
  except ValueError:
      result = False
  return result

def is_valid_hcl(hcl):
  result = True
  if len(hcl) != 7:
    result = False
  if hcl.startswith("#") is False:
    result = False
  if hcl[1:].isalnum is False:
    result = False
  if re.search(r'g-z', hcl[1:]):
    result = False
  return result

def is_valid_pid(pid):
  result = True
  if len(pid) != 9:
    result = False
  if pid.isalnum is False:
    result = False
  return result

def is_valid_passport_part2(passport):
  """Returns True for valid passport, False otherwise.

  Valid passport defined as including the following keys and values:
  byr (Birth Year) 1920 to 2002
  iyr (Issue Year) 2010 to 2020
  eyr (Expiration Year) 2020 to 2030
  hgt (Height) 150 to 193cm or 59 to 76in
  hcl (Hair Color) # followed by 6 characters, 0 to 9 or a to f
  ecl (Eye Color) exactly one of: amb blu brn gry grn hzl oth
  pid (Passport ID) 9 digit number, including leading zeroes

  """
  result = True
  valid_ecls = ('amb', 'blu', 'brn', 'gry', 'grn', 'hzl', 'oth')
  try:
      result = result and is_in_range(passport['byr'], 1920, 2002)
  except KeyError:
      result = False

  try:
      result = result and is_in_range(passport['iyr'], 2010, 2020)
  except KeyError:
      result = False

  try:
      result = result and is_in_range(passport['eyr'], 2020, 2030)
  except KeyError:
      result = False

  try:
      height = passport['hgt']
      min_height = 150
      max_height = 193
      if height[-2:] == 'in':
          min_height = 59
          max_height = 76
      result = result and is_in_range(height[:-2], min_height, max_height)
  except KeyError:
      result = False

  try:
      result = result and passport['ecl'] in valid_ecls
  except KeyError:
      result = False

  try:
      result = result and is_valid_hcl(passport['hcl'])
  except KeyError:
      result = False

  try:
      pid = passport['pid']
      result = result and is_valid_pid(pid)
  except KeyError:
      result = False

  return result

2 valid and 2 invalid passport tests from the instructions:

data_part2 = ["pid:087499704 hgt:74in ecl:grn iyr:2012 eyr:2030 byr:1980",
"hcl:#623a2f",
"",
"eyr:2029 ecl:blu cid:129 byr:1989",
"iyr:2014 pid:896056539 hcl:#a97842 hgt:165cm",
"",
"eyr:1972 cid:100",
"hcl:#18171d ecl:amb hgt:170 pid:186cm iyr:2018 byr:1926",
"",
"iyr:2019",
"hcl:#602927 eyr:1967 hgt:170cm",
"ecl:grn pid:012533040 byr:1946",
]
test_result_part2 = generate_passports(data_part2)

import pprint
pprint.pprint(test_result_part2)

And check that we get 2 valid and 2 invalid results:

[is_valid_passport_part2(passport) for passport in test_result_part2]
len([passport for passport in passports if is_valid_passport_part2(passport)])

Which is the right answer for part 2.