Custom Customs
2020-12-06
Original Prompt Part 1
The input parsing here looks a lot like that in Day 4 - we’ll want to take our big input as a string and split it by the double newline.
Ultimately, we need to flatten each group into one long string and count the unique characters. Splitting each group into a list of characters is straightforward enough:
groups = self.input.split("\n\n")for group in groups: chars = list("".join(group.split("\n")))
['a', 'b', 'c', 'x', 'a', 'b', 'c', 'y', 'a', 'b', 'c', 'z']['a', 'b', 'c']['a', 'b', 'c']['a', 'b', 'a', 'c']['a', 'a', 'a', 'a']['b']
That’s great, but we still need to remove the duplicates. Python’s set
s are perfect for this exact thing. They’re collections (like list
s), but differ in two important ways:
- there are no duplicates (adding an element again is ignored)
- there’s no order to the elements
Sounds perfect for us! Let’s swap set
in for list
:
groups = self.input.split("\n\n")for group in groups: chars = set("".join(group.split("\n")))
{'b', 'y', 'x', 'a', 'z', 'c'}{'b', 'c', 'a'}{'b', 'c', 'a'}{'b', 'c', 'a'}{'a'}{'b'}
Then to get our answer, we need to sum up all of the set sizes. Wrap that in a list comprehension (a succinct way to write loops) and putting that in a sum
call nets us our answer as a one-liner:
return sum( [len(set("".join(group.split("\n")))) for group in self.input.split("\n\n")])
But, for readability’s sake, it’s worth breaking it up a little bit:
groups = self.input.split("\n\n")total = 0for group in groups: all_chars_as_str = "".join(group.split("\n")) total += len(set(all_chars_as_str))return total
Unless you’re literally trying to fit code in the fewest bytes possible, a clear, reable solution is always better than a concise, clever one!
Part 2
For part two, we need to find the letters that appear in all rows of a group. Though we don’t need sets for their de-duplication abilities, they do provide an intersection
method that’ll do exactly what we want:
{1,2,3}.intersection({1, 2, 4}, {1})
{1}
So our setup will be mostly the same as before:
...answers = [set(x) for x in group.split("\n")]
Now, a quandary. To use the .intersection
method, we need a set object to call it on. But if we make a fresh one, then our result will always be empty:
set().intersection({1}, {1})
set() # empty, we wanted {1}
Never fear - we can use the first set in our list as the base:
answers[0].intersection(answers[1:])
Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: unhashable type: 'set'
Python doesn’t like us passing a list
to .intersection
(because list
s are mutable and therefore can’t go into a set). So we need to turn our list
into a bunch of arguments.
Enter Python’s *
operator (sometimes called “splat” or “unpacking” operator). Used with a list
or set
, it provides them individually to a function:
def func(a, b, c): print(a, b, c)
func([1,2,3])
Traceback (most recent call last): File "<stdin>", line 1, in <module>TypeError: func() missing 2 required positional arguments: 'b' and 'c'
func(*[1,2,3])1 2 3
Hey, that’s what we need to do!
groups = self.input.split("\n\n")total = 0for group in groups: answers = [set(x) for x in group.split("\n")] total += len(answers[0].intersection(*answers[1:]))
return total
Python’s *
and **
(not covered here, but it’s got similar behavior, but for dict
s) are powerful tools for programmatically calling functions. Definitely worth reading more about them.