Day eight of AoC 2021 (available at https://adventofcode.com/2021/day/8) throws us into the world of seven-segment displays — those classic digital readouts you see on alarm clocks, calculators, and air conditioners. The catch: the wires are scrambled differently for each display.
Each line of input has 10 signal patterns (the digits 0-9 in scrambled form) and 4 output values we need to decode.
Parsing the Input
interface Mapping {
digits: string[];
values: string[];
}
const processInput = async (day: number) => {
const lines = await readInputLines(day);
return lines.map(line => ({
digits: line.substring(0, 58).split(" ").map(d => d.split("").sort().join("")),
values: line.substring(61).split(" ").map(v => v.split("").sort().join(""))
}));
};
The hardcoded positions (0-58 for digits, 61 onwards for values) are a bit brittle, but the input format is fixed. Each line has exactly 10 digit patterns of known total length, a | separator, then 4 output values.
Critical step: sort each pattern alphabetically. Without this, acf and fca look different when they represent the same digit. Canonical form eliminates ambiguity.
Part One: The Easy Digits
Some digits have unique segment counts:
- 1 uses 2 segments
- 4 uses 4 segments
- 7 uses 3 segments
- 8 uses 7 segments
Part one just asks: how many times do these easy digits appear in the output values?
const partOne = (input: Mapping[], debug: boolean) => {
const uniqueLengths = [2, 3, 4, 7];
return input
.map(m => m.values.filter(v => uniqueLengths.includes(v.length)).length)
.reduce((a, b) => a + b, 0);
};
We don't need to decode anything — just count by length.
Part Two: Full Deduction
Now we need to actually decode each display. This is where it gets interesting.
The approach: use the unique digits (1, 4, 7, 8) to deduce segment mappings, then identify the remaining digits through set operations.
The Deduction Chain
First, identify the easy ones by length:
const one = digits.find(d => d.length === 2)!.split("");
const four = digits.find(d => d.length === 4)!.split("");
const seven = digits.find(d => d.length === 3)!.split("");
const eight = digits.find(d => d.length === 7)!.split("");
The top segment is in 7 but not in 1:
const topSegment = seven.find(s => !one.includes(s))!;
The middle and top-left segments are in 4 but not in 1:
const midLeft = four.filter(s => !one.includes(s));
For the six-segment digits (0, 6, 9), we can identify 9 as the only one containing all segments from both 4 and 7:
const sixes = digits.filter(d => d.length === 6).map(d => d.split(""));
const nine = sixes.find(d =>
seven.every(s => d.includes(s)) && four.every(s => d.includes(s))
)!;
The bottom segment is in 9 but not in 4 or 7:
const bottomSegment = nine.find(s => !seven.includes(s) && !four.includes(s))!;
And so on. Each deduction unlocks the next, like a logic puzzle.
Building the Decoder
After all that deduction, we can construct each digit's canonical form:
const zero = [topSegment, highLeft, highRight, lowLeft, lowRight, bottomSegment].sort().join("");
const one = [...one].sort().join("");
const two = [topSegment, highRight, centerSegment, lowLeft, bottomSegment].sort().join("");
// ... and so on for 3, 4, 5, 6, 7, 8, 9
const digitPatterns = [zero, one, two, three, four, five, six, seven, eight, nine];
Decoding the Output
With the decoder built, converting output values to a number is straightforward:
const outputValue =
digitPatterns.indexOf(values[0]) * 1000 +
digitPatterns.indexOf(values[1]) * 100 +
digitPatterns.indexOf(values[2]) * 10 +
digitPatterns.indexOf(values[3]);
Sum all the output values for the answer.
The Code Is Ugly (And That's OK)
This solution is heavily hardcoded. There's no elegant general algorithm — just a chain of specific deductions based on how seven-segment digits work.
// This is terribly specific but it works
const highRight = one.find(s => !six.includes(s))!;
const lowRight = one.find(s => s !== highRight)!;
In production, I'd never write code this brittle. But for AoC, correctness beats elegance. The puzzle has exactly one structure, and my code solves exactly that structure.
Performance
Both parts run in about 5 milliseconds. The deduction logic is O(1) per display (constant number of comparisons), and we have a few hundred displays. No optimization needed.
Takeaways
Canonical form eliminates ambiguity: Sorting each pattern alphabetically means
acfandfcabecome the same string. This is essential for comparison.Unique properties identify elements: Digit 1 is the only one with 2 segments. Digit 9 is the only 6-segment digit containing all of 4 and 7. Use these unique properties as anchors.
Chain your deductions: Each discovery enables the next. Top segment → bottom segment → nine → high-right segment, and so on.
Hardcoded solutions are fine for puzzles: This code would be unmaintainable in production, but AoC puzzles don't change. Write code that solves the problem in front of you.
Set operations are your friend: "Find the segment in A but not in B" is a set difference. JavaScript's
filterandincludesmake this readable.
The full solution is available in the repository.