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))
)
- Load puzzle input
- Find which 2 values in puzzle input sum to 2020
- Return product of those 2 values
(setq data (read-numbers '"inputs/day1.txt"))
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.
- Find product of the three numbers that sum to 2020
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)))
- Load puzzle input
- 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.
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.
- Load puzzle input
- 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.
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))
)
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).
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.
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.