This year continue solving Advent Of Code in Rust! This is a note of some of my learnings / findings.

Reference

Preparation

Using the following script to download inputs:

#!/usr/local/bin/python3
import argparse
import subprocess
import os

SESSION = os.environ['AOC_SESSION']

parser = argparse.ArgumentParser(description='Read input')
parser.add_argument('day', type=int)
parser.add_argument('--year', type=int, default=2021)
args = parser.parse_args()

cmd = 'curl https://adventofcode.com/{}/day/{}/input --cookie "session={}"'.format(
        args.year, args.day, SESSION)
output = subprocess.check_output(cmd, shell=True)
print(output.decode('utf-8'), end='')

where AOC_SESSION env variable is found in Network tab under headers: cookie -> session=X (see link).

Day 1

  • Learned about tuple_windows from itertools crate to allow iteration over sliding window:
pub fn part2(lines: &Vec<String>) -> i64 {
    let v = lines.iter().map(|l| parse_i64(l));
    let sums = v.tuple_windows().map(|(a, b, c)| a + b + c);
    sums.tuple_windows().filter(|(a, b)| a < b).count() as i64
}

Unfortunately tuple_windows has a limitation of size up to 4. Alternatively one can use slice::windows instead. This becomes a little more verbose as you need to collect elements into a vector before using this API.

pub fn part2(lines: &Vec<String>) -> i64 {
    let v: Vec<_> = lines.iter().map(|l| parse_i64(l)).collect();
    let sums: Vec<_> = v.windows(3).map(|a| a[0] + a[1] + a[2]).collect();
    sums.windows(2).filter(|a| a[0] < a[1]).count() as i64
}

Day 3

  • Rust has a built-in from_str_radix method to convert a binary string to integer:
pub fn from_str_radix(src: &str, radix: u32) -> Result<i64, ParseIntError>
// Example:
i64::from_str_radix("111", 2).unwrap() == 7;
  • Method retain on Vec to quickly filter some elements in-place:
// Example: will keep only elements in values that has i-th bit set to specified value
values.retain(|x| x[i] == bit);

Day 4

  • String has a method split_whitespace to split ignoring any whitespace symbol (not just specific pattern):
let ints: Vec<_> = line.split_whitespace().map(|s| parse_i64(s)).collect();

Day 5

  • Learned how to use regex crate to parse input:
// Example line: 
// 30,443 -> 666,443
let re = Regex::new(r"(\d+),(\d+) -> (\d+),(\d+)").unwrap();
let cap = re.captures(line).unwrap();
let v: Vec<_> = (1..=4).map(|i| parse_i64(&cap[i])).collect();
vents.push((v[0], v[1], v[2], v[3]));

Day 6

  • Recalled how to use map with default values in Rust. A little a bit more verbose than C++, but doesn’t seem like there is more concise way.
// next is a HashMap<i64, i64>
*next.entry(8).or_insert(0) += v;

Day 7

  • Itertools has minmax() trait to compute min and max simultaneously:
let (min, max) = crabs.iter().minmax().into_option().unwrap();

// instead of:
let min = crabs.iter().min().unwrap();
let max = crabs.iter().max().unwrap();

Day 8

  • Itertools has sorted() trait to sort an iterator:
pub fn sorted_digit(s: &str) -> String {
    s.chars().sorted().collect()
}

Day 9

  • Vectors support range indexing and there is product method on iter:
// the same as: flat[0] * flat[1] * flat[2]
flat[0..3].iter().product()

Day 11

  • Iter crate has flatten method to simplify iteration over nested arrays:
// before:
for row in state.iter_mut() {
    for value in row.iter_mut() {
        *value += 1;
    }
}
// after:
state.iter_mut().flatten().for_each(|x| *x += 1);

Day 12

  • Using or_default() is slightly shorter than or_insert(Vec::new()), but it doesn’t infer the type.
// before:
let graph = HashMap::new();
graph.entry(a).or_insert(Vec::new()).push(b);
// after:
let mut graph: HashMap<_, Vec<_>> = HashMap::new();
graph.entry(a).or_default().push(b);

Day 13

  • It is useful to implement my own .cv() method on iterator to collect into vec without any type annotations:
pub trait CollectVec: Iterator + Sized {
    fn cv(self) -> Vec<Self::Item> {
        self.collect()
    }
}
impl<I: Iterator> CollectVec for I {}

Day 14

  • Itertools has counts method to aggregate the iterator into hashmap with occurences
// counts the number of consecutive 2-letter substrings
let mut pairs: HashMap<String, usize> = input.windows(2).map(|x| x.iter().collect()).counts();

Day 15

let mut heap = BinaryHeap::new();

// Wrap values in `Reverse`
heap.push(Reverse(1));
heap.push(Reverse(5));

// If we pop these scores now, they should come back in the reverse order.
assert_eq!(heap.pop(), Some(Reverse(1)));
assert_eq!(heap.pop(), Some(Reverse(2)));

Day 16

'A'.to_digit(16) // returns Some(10)
  • Format string {:04b} to convert from a number to binary representation with leading digits:
format!("{:04b}", 5) // returns "0101"

Day 18

  • Most efficient and universal method in replacing the range of the vector with other elements is splice. This is the example of replacing the splitted value with another pair in day18 problem:
let replace = [Token::Open, Token::Value(a), Token::Value(b), Token::Close];
now.splice(index..=index, replace);