My attempts to work through the 2021 Advent of Code problems.

```commit dd92cf0f8f1931a0e1d5af4203fffd02efec3cd5
parent 993f9555566d97b99f9853aa741fb62f494dc9a1
Date:   Wed,  8 Dec 2021 20:31:39 -0500

Solution to day 8, part 2, plus fixed a mistake in my comments

Diffstat:
Mday08_part1.py | 6++++--
Mutils.py | 5+++++
```
```3 files changed, 146 insertions(+), 2 deletions(-)
diff --git a/day08_part1.py b/day08_part1.py
@@ -20,8 +20,8 @@ displays"""
#   '3' and '4', that's '9'
# * If 6 segments are lit and it's not a '6' or '9', that's '0'
# * If 5 segments are lit and it is missing only one of the segments from '6',
-#   that's '2'
-# * If 5 segments are lit and it's not a '2' or a '3', that's '5'
+#   that's '5'
+# * If 5 segments are lit and it's not a '5' or a '3', that's '2'
# So right now in part 1, it's possible to find the four digits displayed for
# each line of puzzle input! But...
# I'm not going to implement it yet, because "good programmers are lazy", and
@@ -58,6 +58,8 @@ def convert_input_to_df(input_string):
.str.rsplit(' ', 4, expand=True)
.rename(columns=dict(enumerate(('segments', 'digit_1', 'digit_2',
'digit_3', 'digit_4'))))
+        .assign(segments=lambda x: x['segments'].str.replace(' |', '',
+                                                             regex=False))
)
display_df.loc[:, 'digit_1':'digit_4'] = (
display_df.loc[:, 'digit_1':'digit_4'].applymap(frozenset)
diff --git a/day08_part2.py b/day08_part2.py
@@ -0,0 +1,137 @@
+#!/usr/bin/env python
+"""Advent of Code 2021, day 8 (part 2): decode all of the scrambled
+seven-segment displays"""
+
+# This is a fun puzzle. I sat down with pen and paper and proved to myself that
+# you can deduce the mapping from scrambled segments to the full set of digits
+# using just the information provided. It looks like this:
+# * If 2 segments are lit, that's '1'
+# * If 3 segments are lit, that's '7'
+# * If 4 segments are lit, that's '4'
+# * If 7 segments are lit, that's '8'
+# (So far, this was given in the puzzle prompt, nothing new yet)
+# * If 5 segments are lit, it's '2', '3', or '5'
+# * If 6 segments are lit, it's '0', '6', or '9'
+# (Okay, that's not useful... yet)
+# * If 5 segments are lit and they include all the segments in '7', that's '3'
+# * If 6 segments are lit and they don't include all the segments in '7',
+#   that's '6'
+# * If 6 segments are lit and they include the union of the segments comprising
+#   '3' and '4', that's '9'
+# * If 6 segments are lit and it's not a '6' or '9', that's '0'
+# * If 5 segments are lit and it is missing only one of the segments from '6',
+#   that's '5'
+# * If 5 segments are lit and it's not a '2' or a '3', that's '2'
+# So right now in part 1, it's possible to find the four digits displayed for
+# each line of puzzle input! But...
+# I'm not going to implement it yet, because "good programmers are lazy", and
+# maybe part 2 will throw me for a loop? Regardless, figuring this out has
+# influenced my decision about how to represent the data; e.g., we're doing a
+# lot of set operations, so I'm going to use sets for everything, and since
+# it's easy to imagine wanting to use these sets of lit segments as dictionary
+# keys, we'll use immutable `frozenset`s specifically (which can be `dict`
+# keys).
+
+
+from day08_part1 import (EXAMPLE_INPUT,
+                         convert_input_to_df,
+                         apply_mappers_to_digits)
+from utils import get_puzzle_input
+
+def create_mapper_from_segments(segment_sequence):
+    """Given the observed sequence of segment sets (which isn't actually used
+    for anything here), return a function that maps a `frozenset` of segments
+    to a digit"""
+
+    # This approach isn't efficient (or elegant) at all, but I don't think this
+    # function is being run enough times, on a long enough list, to justify the
+    # code complexity for a more efficient/elegant approach
+    segment_sets = [frozenset(i) for i in segment_sequence.split(' ')]
+    digits_to_segments = {}
+
+    # Step 1 of building a segment to digit map: find the mapping to '1', '7',
+    # '4', and '8'
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 2:
+            digits_to_segments['1'] = segment_sets.pop(i)
+            break
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 3:
+            digits_to_segments['7'] = segment_sets.pop(i)
+            break
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 4:
+            digits_to_segments['4'] = segment_sets.pop(i)
+            break
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 7:
+            digits_to_segments['8'] = segment_sets.pop(i)
+            break
+
+    # Step 2: using '7' alone, find '3' and '6'
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 5 and not digits_to_segments['7'] - seg:
+            digits_to_segments['3'] = segment_sets.pop(i)
+            break
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 6 and (digits_to_segments['7'] - seg):
+            digits_to_segments['6'] = segment_sets.pop(i)
+            break
+
+    # Step 3: Using '4', '3', and '6', find '9', and '5'
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 5 and len(digits_to_segments['6'] - seg) == 1:
+            digits_to_segments['5'] = segment_sets.pop(i)
+            break
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 6 and \
+                seg == digits_to_segments['3'] | digits_to_segments['4']:
+            digits_to_segments['9'] = segment_sets.pop(i)
+            break
+
+    # Step 4 (final): Using '3', '6', '2', and '9', find '0' and '2'
+    for i, seg in enumerate(segment_sets):
+        if len(seg) == 5:
+            digits_to_segments['2'] = segment_sets.pop(i)
+            break
+    digits_to_segments['0'] = segment_sets.pop()
+
+    segments_to_digits = {v:k for k, v in digits_to_segments.items()}
+
+    def part_2_mapper(segments):
+        """Just a closure wrapping the dictionary (hackily) built above"""
+        return segments_to_digits.get(segments)
+    return part_2_mapper
+
+def convert_single_digits_to_integers(digits_df):
+    """Given a four-column df of digit displays, return a Series consisting of
+    a the single integer value obtained across each row"""
+    # Can this be done with `apply` somehow? This feels needlessly inelegant
+    return (
+        digits_df['digit_1']
+        .astype(int)
+    )
+
+def solve_puzzle(input_string):
+    """Return the numeric solution to the puzzle"""
+    return int(
+        convert_single_digits_to_integers(
+            apply_mappers_to_digits(
+                convert_input_to_df(input_string),
+                create_mapper_from_segments
+            )
+        )
+        .sum()
+    )
+
+def main():
+    """Run when the file is called as a script"""
+    assert solve_puzzle(EXAMPLE_INPUT) == 61229
+    print("Sum of all decoded displays:",
+          solve_puzzle(get_puzzle_input(8)))
+
+if __name__ == "__main__":
+    main()
diff --git a/utils.py b/utils.py
@@ -5,6 +5,11 @@ import requests
import numpy as np
import pandas as pd

+def convert_lines_to_series(input_string):
+    """Return a pandas Series consisting of the lines in the
+    (newline-delimited) input string"""
+    return pd.Series(input_string.rstrip('\n').split('\n'))
+
def convert_int_line_to_series(input_string):
"""Converts one (optionally newline-terminated) string of comma-separated
integers to a pandas Series"""
```