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 sets are perfect for this exact thing. They’re collections (like lists), 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 totalUnless 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 lists 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 3Hey, 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 totalPython’s * and ** (not covered here, but it’s got similar behavior, but for dicts) are powerful tools for programmatically calling functions. Definitely worth reading more about them.