Skip to Content

Advent of Code 2021 - Day 8, in Kotlin - Seven Segment Search

Kotlin solutions to parts 1 and 2 of Advent of Code 2021, Day 8: 'Seven Segment Search'

Posted on

I think this is my favorite puzzle of 2021 so far, I enjoyed solving part two especially. To me, it was a good lesson in reading the requirements as requirements and not as instructions.

If you’d rather just view code, the GitHub Repository is here .

Problem Input

We need to look at our input row-by-row, effectively solving the same puzzle a few times. The rows have nothing to do with each other at all. So let’s pull our input into a class called InputRow, which will hold a single row of input and later, allow us to perform some functions on a row.

We’ll start by importing our raw input as a List<String> and parsing them into a List<InputRow> as inputRows.

class Day08(input: List<String>) {

    private val inputRows = input.map { InputRow.of(it) }

As for InputRow, we’ll define that within Day08 so we can keep it private and not interfere with other daily solution classes. As is my usual preference, we’ll define a companion object with an of function to handle the parsing:

// In Day08

private class InputRow(val digitSegments: List<Set<Char>>) {

    companion object {
        fun of(input: String): InputRow =
            InputRow(
                input.split(" ").filterNot { it == "|" }.map { it.toSet() }
            )
    }
}

You might notice that we don’t store the digit segments as a String, but instead as a Set<Char>. This helps us because we don’t really care about the order of the lit segments of a digit, just the presence. A String or a List<Char> would impose an order where we don’t actually want one.

⭐ Day 8, Part 1

The puzzle text can be found here.

Because we did a lot of working in the parsing section, solving part one is fairly straight forward:

// In Day08

fun solvePart1(): Int =
    inputRows.sumOf { row ->
        row.digitSegments.takeLast(4).count { it.size in setOf(2, 3, 4, 7) }
    }

We look at all the inputRows and get the sumOf the count of the number of segments that have unique lengths (2, 3, 4, or 7). We constrain this to the last 4 elements in the list.

Star earned! Onward!

⭐ Day 8, Part 2

The puzzle text can be found here.

This is one of the very rare cases where I have been able to guess what part two is before seeing it. If we read the instructions we might get the impression that we need to get down to the segment level (a, b, c, etc) and map them to locations within the seven segment display. However, if we look at the displays as they are rendered, we can see that some numbers overlap others.

For example, here are 3 and 5:

  1:        3:
 ....      aaaa   
.    c    .    c  
.    c    .    c  
 ....      dddd    
.    f    .    f  
.    f    .    f  
 ....      gggg   

Sure, they share segments c and f, but more interestingly is that 3 covers 1. Meaning, the segments that make up 3 are a superset of the segments that make up 1. Also notice that 3 has 5 lit segments. Of all the digits with 5 lit segments (which are 2, 3, and 5), 3 is the only one that is a superset of 1. Therefore, if we know 1, we can find 3. And we know 1.

Let’s do this for the rest of the numbers and figure out their unique properties:

Digit Segments Lit Properties
0 7 Overlaps 1 and 7
1 2 Unique size
2 5 Not 3 or 5
3 5 Overlaps 1
4 4 Unique size
5 6 Overlapped by 6
6 6 Overlaps 6
7 3 Unique size
8 7 Unique size
9 6 Overlaps 1, 3, 4, 5, and 7

Let’s use this chart to implement our digit discovery logic, which we will write as a function in InputRow:

// In InputRow
private val digitValues = discoverMappings()

private fun discoverMappings(): Map<Set<Char>, Int> {
    val digitToString = Array<Set<Char>>(10) { emptySet() }

    // Unique based on size
    digitToString[1] = digitSegments.first { it.size == 2 }
    digitToString[4] = digitSegments.first { it.size == 4 }
    digitToString[7] = digitSegments.first { it.size == 3 }
    digitToString[8] = digitSegments.first { it.size == 7 }

    // 3 is length 5 and overlaps 1
    digitToString[3] = digitSegments
        .filter { it.size == 5 }
        .first { it overlaps digitToString[1] }

    // 9 is length 6 and overlaps 3
    digitToString[9] = digitSegments
        .filter { it.size == 6 }
        .first { it overlaps digitToString[3] }

    // 0 is length 6, overlaps 1 and 7, and is not 9
    digitToString[0] = digitSegments
        .filter { it.size == 6 }
        .filter { it overlaps digitToString[1] && it overlaps digitToString[7] }
        .first { it != digitToString[9] }

    // 6 is length 6 and is not 0 or 9
    digitToString[6] = digitSegments
        .filter { it.size == 6 }
        .first { it != digitToString[0] && it != digitToString[9] }

    // 5 is length 5 and is overlapped by 6
    digitToString[5] = digitSegments
        .filter { it.size == 5 }
        .first { digitToString[6] overlaps it }

    // 2 is length 5 and is not 3 or 5
    digitToString[2] = digitSegments
        .filter { it.size == 5 }
        .first { it != digitToString[3] && it != digitToString[5] }

    return digitToString.mapIndexed { index, chars -> chars to index }.toMap()
}

We’ll declare a digitToString array in order to hold our working data. As we discover digits we’ll replace the default emptySet with the Set<Char> for the digit we found. We could have gone with nullable values in the array here but I don’t really like nulls so we’ll populate the array with empty sets instead.

First, we’ll get digits we can identify based on their size alone. Then we’ll go through the digits one by one and find them according to the criteria in the table above. Note that order is somewhat important - we can’t check for overlaps unless we know one of the digits. Similarly, in the case with 2, we can’t identify that until we’ve identified all of the other 5-segment digits.

At the end of this function we’ll turn our array into a Map<Set<Char>, Int>. This will let us look up digits based on sets of characters in that digits representation, which we will store in digitValues in InputRow.

As for overlap, we could have written this inline but I thought an infix extension function looked nicer:

// In InputRow

private infix fun Set<Char>.overlaps(that: Set<Char>): Boolean =
    this.containsAll(that)

We also need a way to actually calculate the answer:

// In InputRow

fun calculateValue(): Int =
    (digitValues.getValue(digitSegments[10]) * 1000) +
        (digitValues.getValue(digitSegments[11]) * 100) +
        (digitValues.getValue(digitSegments[12]) * 10) +
        digitValues.getValue(digitSegments[13])

Note that this could have been a property too, since our InputRow is immutable and we always want to know this answer (in part two anyway).

Summing the values of each of our InputRows gives us the answer to part two:

// In Day08

fun solvePart2(): Int = inputRows.sumOf { row ->
    row.calculateValue()
}

Star earned!

Further Reading

  1. Index of All Solutions - All posts and solutions for 2021, in Kotlin.
  2. My Github repo - Solutions and tests for each day.
  3. Solution - Full code for day 8
  4. Advent of Code - Come join in and do these challenges yourself!