After a relatively easy part 1, the part 2 was the most challenging since the beginning of this Advent of Code.

I spent about 50 minutes for part 1, and nearly 3 hours for part 2. My ranks are roughly the same for both parts, just below the 10000th place. As I am writing this article, at 10 pm (UTC +1), more than 30 % of part 1 solvers have not been able to solve the part 2.

Problem description

The problem objective is to decode signals, and to re-order 7-segment displays. Each line in the input file is composed of a series of 10 signal patterns followed by a delimiter and a second series of 4 patterns. In the first series, the patterns represent the 10 different digits, and each pattern indicates which segments of a 7-segment display are on (between 2 and 7 segments). The second series represent a 4-digit output value, using the same code as the input series.

Below is an example of a input file line.

acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab | cdfeb fcadb cdfeb cdbaf

The correspondance between digits and patterns is the following :

Pattern Digit
acedgfb 8
cdfbe 5
gcdfa 2
fbcad 3
dab 7
cefabd 9
cdfgeb 6
eafb 4
cagedb 0
ab 1

Therefore, the output number is 5353.

Note that each line of the input data file has a different code.

Data structures

Input and output signals are separated in two distinct vectors.

#[derive(Clone)]
struct Entry {
    input_signal: Vec<String>,
    output_signal: Vec<String>
}

impl Entry {
    pub fn new(input: String) -> Self {
        let entry_parts = input.split(" | ").collect::<Vec<&str>>();

        Entry { input_signal: entry_parts[0].split(" ")
                .map(|x| String::from(x)).collect::<Vec<String>>(), 
            output_signal: entry_parts[1].split(" ")
                .map(|x| String::from(x)).collect::<Vec<String>>() } 
    }
}

Part 1 (easy)

The goal is to count output digits equal to 1, 4, 7 or 8. The simple idea is that these digits can be recognized by their number of segments (for example, the digit 1 is the only one with 2 segments)

We use the following table :

Digit Number of segments
0 6
1 2
2 5
3 5
4 4
5 5
6 6
7 3
8 7
9 6

and we add the following method to Entry

pub fn count_easy_digits(&self) -> usize {
	self.output_signal.iter().cloned()
		.filter(|x| x.len() == 2 || x.len() == 3 || 
			x.len() == 4 || x.len() == 7).count()
}

The main function sums the outcomes of count_easy_digits() for each line.

pub fn solve_part1(input: &Vec<String>) -> usize {
    let mut easy_digits_count = 0;
    for line in input {
        easy_digits_count += Entry::new(line.to_string()).count_easy_digits();
    }
    easy_digits_count
}

Part 2 (challenging)

We write a method correspondence_table(), that returns a vector of 10 elements. Each element is a String, containing the digit segments.

The decoding operation is done in 3 steps !

let mut correspondence = vec![""; 10]; 

for pattern in &self.input_signal {
    match pattern.len() {
        2 => { correspondence[1] = pattern; }
        3 => { correspondence[7] = pattern; }
        4 => { correspondence[4] = pattern; }
        7 => { correspondence[8] = pattern; }
        _ => {}
    }
}
let segment1_count = self.input_signal.iter().cloned()
    .filter(|x| x.as_str().contains(nth_char(correspondence[1], 0))).count();
let segment2_count = self.input_signal.iter().cloned()
    .filter(|x| x.as_str().contains(nth_char(correspondence[1], 1))).count();
let segments = match (segment1_count, segment2_count) {
    (8, 9) => Ok((nth_char(correspondence[1], 0), nth_char(correspondence[1], 1))),
    (9, 8) => Ok((nth_char(correspondence[1], 1), nth_char(correspondence[1], 0))),
    _ => Err("Cannot identify segments")
};
let general_set = "abcdefg".chars().collect::<HashSet<char>>();
for pattern in &self.input_signal {
    if pattern.len() == 5 {
        match (pattern.as_str().contains(segments.unwrap().0),
            pattern.as_str().contains(segments.unwrap().1)) {
            (true, true) => { correspondence[3] = pattern; }
            (true, false) => { correspondence[2] = pattern; }
            (false, true) => { correspondence[5] = pattern; }
            (false, false) => { panic!(); }
        }
    }
    if pattern.len() == 6 {
        let pattern_set = pattern.as_str().chars().collect::<HashSet<char>>();
        let missing = general_set.difference(&pattern_set).next().unwrap();
        let missing_count = self.input_signal.iter()
            .filter(|&x| x.as_str().find(|c| c == *missing).is_some()).count();
        match missing_count {
            4 => { correspondence[9] = pattern; }
            7 => { correspondence[0] = pattern; }
            8 => { correspondence[6] = pattern; }
            _ => { panic!("Cannot identify segemnts"); }
        }
    }
}

Before returning the result, we check if all digits have been identified. If not, that would mean that there is a bug in either our code, or in the input file.

The most difficult part is done, we just have to decode the output signal with our correspondence table.

pub fn output_number(&self) -> u64 {
	let mut number = 0;
	let correspondence = self.correspondence_table();

	for pattern in &self.output_signal {
	    let sorted = chars_sort(pattern.to_string());
	    for i in 0..10 {
		if sorted == chars_sort(correspondence[i].to_string()) {
		    number = number * 10 + (i as u64);
		    break;
		}
	    }
	}

	number
}

Finally, in the main function, we sum the output numbers.

pub fn solve_part2(input: &Vec<String>) -> u64 {
    let mut entries_sum = 0;
    for line in input {
        entries_sum += Entry::new(line.to_string()).output_number();
    }

    entries_sum
}

I had many difficulties with Rust String methods, and I lost way too much time trying to compile the code. Then, I forgot the check described in step 3 description, and I gave a wrong answer on my first attempt, although my unit tests passed.