Seven Segment Search

Not much commentary on this one because it’s pretty straightforward.

The description for this task was one of the most convoluted and confusing ones yet. Well done on the obfuscation part.

Prelude

aoc = __import__('aoc-common').aoc()
import numpy as np
import re
from collections import Counter
from functools import reduce

text = aoc.input_text(8)

Part 1

Count number of strings of certain lengths. One of the simpler tasks.

obs, nums = zip(*(l.split(' | ') for l in text.split('\n') if l))
counts = Counter(map(len, ' '.join(nums).split()))

# Doing the above (joining the strings and re-splitting) is better than doing
# updates on each sub-string. (See my "rant" linked on AoC day 3.)

answer = sum(counts[x] for x in [2,4,3,7])
print(f'answer to part 1: {answer}')

Part 2

Task: reason about seven-segment display to figure out what numbers are displayed on a broken display.

I chose to make a utility class here, even though it’s not really necessary. But this is one of the great strengths of Python and other languages that allow customizing things like numbers, iterables, maps, and so on — you can approximate domain specific languages and make things ten times more ergonomic and, well, pleasant to work with.

class segments(int):
  """Utility class because I'm a huge fan of DSLs.

  >>> abd = segments.from_str('abd')
  >>> acdg = segments.from_str('agdc')
  >>> abd - acdg
  b
  >>> abd + acdg
  abcdg
  >>> ~abd
  cefg
  >>> ~abd & acdg
  cg
  >>> list(abd)
  [a, b, d]

  """
  def __repr__(self):
    return ''.join(chr(97 + i) for i in range(7) if self & 1 << i)
  def __str__(self):
    return repr(self)

  @staticmethod
  def from_str(s):
    return segments(reduce(lambda x,y: x|y, (1 << ord(x) - 97 for x in s.lower())))

  __add__ = lambda x, y: x|y
  __sub__ = lambda x, y: x^(x&y)
  __neg__ = lambda x: ~x
  __not__ = lambda x: ~x

  def __or__(x,y): return segments(int.__or__(x, y))
  def __and__(x,y): return segments(int.__and__(x, y))
  def __xor__(x,y): return segments(int.__xor__(x, y))
  def __invert__(x): return x^127

  def __iter__(self):
    return (segments(self & 1 << i) for i in range(7) if self & 1 << i)

Making these classes is nearly automatic and by instinct now, after so many CTFs. You always want to have something you can play around with in the REPL when investigating some problem or group:

>>> a,b,c,d,e,f,g,all = map(segments, (1,2,4,8,16,32,64,127))
>>> a+b+c
abc
>>> all&(b+c+d) - d
bc
>>> [a + c for x in all]
[ac, ac, ac, ac, ac, ac, ac]

Anyway, the actual reasoning I mapped out in a comment and then the implementation is just straightforward:


# Some notes while I work this out.
#
#  aaaa
# b    c
# b    c
#  dddd
# e    f
# e    f
#  gggg
#
# L2 = cf (1)
# L3 = acf (7)
# L4 = bcdf (4)
# L7 = <all> (8)
#
# L5 = acdeg acdfg abdfg (235)
# L6 = abcefg abdefg abcdfg (069)
#
# cf = L2
# a = L3 - L2
# bd = L4 - L2
# dg = (only L5 & cf) - cf - a
# d = dg & bd
# g = dg - d
# b = bd - d
#
# 0 is only L6 without d, 2 is only L5 without b

class resolver:
  """Fairly useless to pack this in a class, but unfortunately it's the best way
  in Python to get some isolated code with state.

  """
  def __init__(self, obs):
    self._by_len = dict()
    for x in obs:
      self._by_len.setdefault(len(x), []).append(segments.from_str(x))
    self._digits = dict()
    self._analyze()

  def __call__(self, num):
    return self._digits[segments.from_str(num)]

  def _analyze(self):
    cf = self._discover(1, 2)
    a = self._discover(7, 3) - cf
    bd = self._discover(4, 4) - cf
    self._discover(8, 7)

    dg = self._discover(3, 5, has=cf) - cf - a

    d = dg & bd
    self._discover(0, 6, hasnt=d)
    self._discover(9, 6, has=cf)
    self._discover(6, 6) # only choice left

    b = bd - d
    self._discover(2, 5, hasnt=b)
    self._discover(5, 5) # only choice left.


  def _discover(self, d, l, has=None, hasnt=None):
    sel = [x for x in self._by_len[l] if (has is None or has & x == has) and (hasnt is None or ~x & hasnt == hasnt)]
    assert len(sel) == 1
    self._by_len[l].remove(sel[0])
    self._digits[sel[0]] = d
    return sel[0]


total = 0
for obs_txt, num_txt in zip(obs, nums):
  r = resolver(obs_txt.split())
  total += sum(r(n) * 10**i for i,n in enumerate(num_txt.split()[::-1]))

print(f'answer to part 2: {total}')