Advent of Code 2022: The rest of Week 1

Published December 9, 2022 on Chandler Swift's Blog Source


Day 2

I worked my way through the first half of the Emacs tutorial: document navigation, basic text insertion and deletion, cut and paste, and undo/redo. Still no syntax highlighting or anything fancy. I’d guess about two hours for the tutorial, and 20 minutes for writing code.

Part 1 source code
use std::fs;

fn main() {
    let data = fs::read_to_string("input.txt").expect("Could not read input.txt");
    let data = data.trim();

    let mut score = 0;

    for line in data.split("\n") {
        let choices: Vec<&str> = line.split(" ").collect();
        score += match (choices[0], choices[1]) {
            ("A", "X") => 1 + 3,
            ("A", "Y") => 2 + 6,
            ("A", "Z") => 3 + 0,
            ("B", "X") => 1 + 0,
            ("B", "Y") => 2 + 3,
            ("B", "Z") => 3 + 6,
            ("C", "X") => 1 + 6,
            ("C", "Y") => 2 + 0,
            ("C", "Z") => 3 + 3,
            _ => panic!("Unknown combination {:?}", choices),
        };
    }
    println!("{}", score);
}
Part 2 source code
use std::fs;

fn main() {
    let data = fs::read_to_string("input.txt").expect("Could not read input.txt");
    let data = data.trim();

    let mut score = 0;

    for line in data.split("\n") {
        let choices: Vec<&str> = line.split(" ").collect();
        score += match (choices[0], choices[1]) {
            ("A", "X") => 3 + 0,
            ("A", "Y") => 1 + 3,
            ("A", "Z") => 2 + 6,
            ("B", "X") => 1 + 0,
            ("B", "Y") => 2 + 3,
            ("B", "Z") => 3 + 6,
            ("C", "X") => 2 + 0,
            ("C", "Y") => 3 + 3,
            ("C", "Z") => 1 + 6,
            _ => panic!("Unknown combination {:?}", choices),
        };
    }
    println!("{}", score);
}

Day 3

I completed the rest of the Emacs tutorial: Buffers and buffer manipulation, modes, search, window/frame management, and the help system. I’m definitely feeling a bit more confident in my basic editor usage. Still leaving more advanced editor configuration for tomorrow. I spent about two more hours in the tutorial today, and a hair over 30 minutes writing code.

Part 1 source code
use std::fs;

fn find_duplicated_char(first: &str, second: &str) -> char {
    for char in first.chars() {
        if second.contains(char) {
            return char;
        }
    }
    panic!("no duplicate found");
}

fn priority(c: char) -> u32 {
    match c {
        'A'..='Z' => c as u32 - 64 + 26,
        'a'..='z' => c as u32 - 96,
        _ => panic!("This isn't a letter!"),
    }
}

fn process(data: &str) -> u32 {
    let mut score = 0;
    for line in data.split("\n") {
        let first_half = &line[0..line.len() / 2];
        let second_half = &line[line.len() / 2..line.len()];
        score += priority(find_duplicated_char(first_half, second_half));
    }
    score
}

fn main() {
    let data = fs::read_to_string("input.txt").expect("Could not read input.txt");
    let data = data.trim();

    println!("{}", process(data));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test() {
        let data = "vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw";
        assert_eq!(process(data), 157);
    }
}
Part 2 source code
use std::fs;

fn find_duplicated_char(first: &str, second: &str, third: &str) -> char {
    for char in first.chars() {
        if second.contains(char) && third.contains(char) {
            return char;
        }
    }
    panic!("no duplicate found");
}

fn priority(c: char) -> u32 {
    match c {
        'A'..='Z' => c as u32 - 64 + 26,
        'a'..='z' => c as u32 - 96,
        _ => panic!("This isn't a letter!"),
    }
}

fn process(data: &str) -> u32 {
    let mut score = 0;
    let lines: Vec<&str> = data.split("\n").collect();
    let mut i = 0;
    while i < lines.len() {
        score += priority(find_duplicated_char(lines[i], lines[i + 1], lines[i + 2]));
        i += 3;
    }
    score
}

fn main() {
    let data = fs::read_to_string("input.txt").expect("Could not read input.txt");
    let data = data.trim();

    println!("{}", process(data));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test() {
        let data = "vJrwpWtwJgWrhcsFMMfFFhFp
jqHRNqRjqzjGDLGLrsFMfFZSrLrFZsSL
PmmdzqPrVvPwwTWBwg
wMqvLMZHhHMvwLHjbvcjnnSBnvTQFn
ttgJtRGJQctTZtZT
CrZsJsPPZsGzwwsLwLmpwMDw";
        assert_eq!(process(data), 70);
    }
}

Day 4

Today I read the info pages for Dired and figured out that M-! executes shell commands, so I did the entirety of my work in Rust – no external terminal needed! I’m still writing the blog post in VS Code, but I’m starting to C-x C-s to save there, which, of course doesn’t work!

Part 1 source code
use std::fs;

fn process(data: &str) -> u32 {
    let mut contains_count = 0;
    for line in data.split("\n") {
        let ranges: Vec<&str> = line.split(",").collect();
        let first: Vec<&str> = ranges[0].split("-").collect();
        let second: Vec<&str> = ranges[1].split("-").collect();
        let first_start: u32 = first[0].parse().unwrap();
        let first_end: u32 = first[1].parse().unwrap();
        let second_start: u32 = second[0].parse().unwrap();
        let second_end: u32 = second[1].parse().unwrap();
        if (first_start <= second_start && first_end >= second_end)
            || (second_start <= first_start && second_end >= first_end)
        {
            contains_count += 1;
        }
    }
    contains_count
}

fn main() {
    let data = fs::read_to_string("input.txt").expect("Could not read input.txt");
    let data = data.trim();

    println!("{}", process(data));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test() {
        let data = "2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8";
        assert_eq!(process(data), 2);
    }
}
Part 2 source code
use std::fs;

fn process(data: &str) -> u32 {
    let mut contains_count = 0;
    for line in data.split("\n") {
        let ranges: Vec<&str> = line.split(",").collect();
        let first: Vec<&str> = ranges[0].split("-").collect();
        let second: Vec<&str> = ranges[1].split("-").collect();
        let first_start: u32 = first[0].parse().unwrap();
        let first_end: u32 = first[1].parse().unwrap();
        let second_start: u32 = second[0].parse().unwrap();
        let second_end: u32 = second[1].parse().unwrap();
        if first_start <= second_end && second_start <= first_end {
            contains_count += 1;
        }
    }
    contains_count
}

fn main() {
    let data = fs::read_to_string("input.txt").expect("Could not read input.txt");
    let data = data.trim();

    println!("{}", process(data));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test() {
        let data = "2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8";
        assert_eq!(process(data), 4);
    }
}

Screenshot of my Emacs setup

Day 5

I didn’t make any meaningful Emacs progress today, other than reading the help as I needed for specific commands. I’m starting to think about the types of things I might want in an Emacs config. And I’m reliably C-x C-sing in VS Code now.

Part 1 source code
use std::collections::VecDeque;
use std::fs;

fn process(data: &str) -> String {
    let parts: Vec<&str> = data.split("\n\n").collect();

    let mut stack_lines: Vec<&str> = parts[0].split("\n").collect();
    stack_lines.truncate(stack_lines.len() - 1); // Remove indexing line

    let instructions: Vec<&str> = parts[1].trim().split("\n").collect();

    let mut stacks = Vec::new();
    for _ in 0..(stack_lines[0].len() + 1) / 4 {
        stacks.push(VecDeque::new());
    }

    for line in stack_lines {
        for i in 0..stacks.len() {
            let c = line.chars().nth(i * 4 + 1).unwrap();
            if c != ' ' {
                stacks[i].push_back(c);
            }
        }
    }

    for instruction in instructions {
        let instruction: Vec<&str> = instruction.split(" ").collect();
        let source: usize = instruction[3].parse().unwrap();
        let target: usize = instruction[5].parse().unwrap();
        for _ in 0..instruction[1].parse().unwrap() {
            let c = stacks[source-1].pop_front().unwrap();
            stacks[target-1].push_front(c);
        }
    }
    stacks.iter().map(|s| s.front().unwrap()).collect()
}

fn main() {
    let data = fs::read_to_string("input.txt").unwrap();

    println!("{}", process(&data));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test() {
        let data = "    [D]    
[N] [C]    
[Z] [M] [P]
 1   2   3 

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2";
        assert!(process(data) == "CMZ");
    }
}
Part 2 source code
use std::collections::VecDeque;
use std::fs;

fn process(data: &str) -> String {
    let parts: Vec<&str> = data.split("\n\n").collect();

    let mut stack_lines: Vec<&str> = parts[0].split("\n").collect();
    stack_lines.truncate(stack_lines.len() - 1); // Remove indexing line

    let instructions: Vec<&str> = parts[1].trim().split("\n").collect();

    let mut stacks = Vec::new();
    for _ in 0..(stack_lines[0].len() + 1) / 4 {
        stacks.push(VecDeque::new());
    }

    for line in stack_lines {
        for i in 0..stacks.len() {
            let c = line.chars().nth(i * 4 + 1).unwrap();
            if c != ' ' {
                stacks[i].push_back(c);
            }
        }
    }

    for instruction in instructions {
        let instruction: Vec<&str> = instruction.split(" ").collect();
        let source: usize = instruction[3].parse().unwrap();
        let target: usize = instruction[5].parse().unwrap();
        for i in 0..instruction[1].parse().unwrap() {
            let c = stacks[source-1].pop_front().unwrap();
            stacks[target-1].insert(i, c);
        }
    }
    stacks.iter().map(|s| s.front().unwrap()).collect()
}

fn main() {
    let data = fs::read_to_string("input.txt").unwrap();

    println!("{}", process(&data));
}

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test() {
        let data = "    [D]    
[N] [C]    
[Z] [M] [P]
 1   2   3 

move 1 from 2 to 1
move 3 from 1 to 3
move 2 from 2 to 1
move 1 from 1 to 2";
        assert!(process(data) == "MCD");
    }
}

Day 6

Today was the first day that I was a bit stumped writing code. The uniqueness check took a while to ponder through, but I got it working in just under an hour. Again, no major progress on anything Emacs-related.

Source code
use std::fs;

fn process(data: &str, len: usize) -> usize {
    let chars: Vec<char> = data.chars().collect();
    for i in len..chars.len() {
        let mut chars = chars[i-len..i].to_vec(); // TODO: this seems unidiomatic?
        chars.sort();
        chars.dedup();
        if chars.len() == len {
            // no duplicates
            return i;
        }
    }
    panic!("No valid marker found");
}

fn main() {
    let data = fs::read_to_string("input.txt").unwrap();
    let data = data.trim();
    println!("{}", process(data, 14)); // 4 for part A
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]
    fn test_short() {
        let data = vec![
            ("mjqjpqmgbljsphdztnvjfqwrcgsmlb", 7),
            ("bvwbjplbgvbhsrlpgdmjqwftvncz", 5),
            ("nppdvjthqldpwncqszvftbrmjlhg", 6),
            ("nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg", 10),
            ("zcfzfwzzqfrljwzlrfnpqdbhtmscgvjw", 11),
        ];
        for (datum, result) in data {
            assert!(process(datum, 4) == result);
        }
    }
    #[test]
    fn test_long() {
        let data = vec![
            ("mjqjpqmgbljsphdztnvjfqwrcgsmlb", 19),
            ("bvwbjplbgvbhsrlpgdmjqwftvncz", 23),
            ("nppdvjthqldpwncqszvftbrmjlhg", 23),
            ("nznrnfrfntjfmvfwmzdfjlvtqnbhcprsg", 29),
            ("zcfzfwzzqfrljwzlrfnpqdbhtmscgvjw", 26),
        ];
        for (datum, result) in data {
            assert!(process(datum, 14) == result);
        }
    }
}

Day 7

I finally installed rust-mode. I’m not sure why I waited so long—this was incredibly simple. M-x package-install rust-mode and then M-x rust-mode had me in syntax-highlighted, working-indentation paradise1 in a grand total of maybe three minutes. (This only works in Emacs 28 and later, which includes the nongnu ELPA repos, and, therefore, rust-mode).

This problem took me quite a long time; I spent a lot of time figuring out how to best parse this data, and then a lot of additional time fighting negotiating with the borrow checker. A first iteration involved mutual references (e.g. for directories A and B, with B a subdirectory of A, A would have B in its list of children, and B would have A listed for its parent ("..")), which doesn’t work. I looked into Rc and RefCells, but ended up coming up with a moderately straightforward implementation that didn’t use either. I also had an epiphany about how borrowing works that finally (after an embarrassingly long time) made my mental model of the borrow checker click into place. So, all in all, definitely worth the struggle today!

I’d guess this puzzle took me three to four hours of intermittent work, over three days, which is why this post came out a bit later than targeted. Hopefully now that this one is done, I’ll be back on track going forward!

Source code
use std::collections::HashMap;
use std::fs;

struct Directory {
    children: HashMap<String, Directory>,
    files: HashMap<String, u32>,
}

impl Directory {
    fn size(&self) -> u32 {
        let mut size = 0;
        for (_name, child) in &self.children {
            size += child.size();
        }
        for (_name, file_size) in &self.files {
            size += file_size;
        }
        size
    }

    fn directories_at_most_size(&self, size: u32) -> u32 {
        let mut count = if self.size() <= size { self.size() } else { 0 };
        for (_name, child) in &self.children {
            count += child.directories_at_most_size(size);
        }
        count
    }

    fn smallest_directory_at_least(&self, target_size: u32) -> u32 {
        let mut size = u32::MAX;
        if self.size() < size && self.size() >= target_size {
            size = self.size();
        }
        for (_name, child) in &self.children {
            let child_size = child.smallest_directory_at_least(target_size);
            if child_size < size && child_size >= target_size {
                size = child_size;
            }
        }
        size
    }
}

fn process(data: &str) -> u32 {
    let lines: Vec<&str> = data.split("\n").collect();
    let mut root = Directory {
        children: HashMap::new(),
        files: HashMap::new(),
    };
    let mut path = Vec::new();
    for line in lines {
        let line: Vec<&str> = line.split(" ").collect();
        if line[0] == "$" {
            match line[1] {
                "cd" => match line[2] {
                    "/" => path = Vec::new(),
                    ".." => path.truncate(path.len() - 1),
                    dir => path.push(dir),
                },
                "ls" => {}
                _ => panic!("Unknown command"),
            }
        } else {
            // Part of `ls` output
            let mut pwd = &mut root;
            for dir in path.iter() {
                pwd = pwd.children.get_mut(&dir.to_string()).unwrap();
            }
            if line[0] == "dir" {
                if pwd.children.contains_key(line[1]) {
                    println!("We've seen {} before", line[1]);
                } else {
                    pwd.children.insert(
                        line[1].to_string(),
                        Directory {
                            children: HashMap::new(),
                            files: HashMap::new(),
                        },
                    );
                }
            } else {
                pwd.files
                    .insert(line[1].to_string(), line[0].parse().unwrap());
            }
        }
    }
    let size_available = 70_000_000 - root.size();
    let size_needed = 30_000_000 - size_available;
    root.smallest_directory_at_least(size_needed) // directories_at_most_size() for part 1
}

fn main() {
    let data = fs::read_to_string("input.txt").unwrap();
    let data = data.trim();
    println!("{}", process(data));
}

#[cfg(test)]
mod test {
    use super::*;

    static DATA: &str = "$ cd /
$ ls
dir a
14848514 b.txt
8504156 c.dat
dir d
$ cd a
$ ls
dir e
29116 f
2557 g
62596 h.lst
$ cd e
$ ls
584 i
$ cd ..
$ cd ..
$ cd d
$ ls
4060174 j
8033020 d.log
5626152 d.ext
7214296 k";

    #[test]
    fn test() {
        assert!(process(DATA) == 24933642); // 95437 for part 1
    }
}

Screenshot of my Emacs setup

Notes

At this point, I’m maintaining a bit of a checklist of things I want to figure out how to do in Emacs, and I’m making a bit of progress:

  • Use Dired instead of a terminal or file manager
  • Rust syntax highlighting1
  • Rust display errors inline
  • Fix wonky indentation (partially—spaces/tabs are still mixed)
    • Run rustfmt automatically?
  • Single key combination to compile and run binary
  • Git integration within Emacs, without falling back to M-x shell
  • Modify config file (aka learn elisp)
    • Set auto-revert-mode by default (useful for e.g. rustfmt)

I’ve really grown to like a lot of things about Emacs:

  • Emacs’ help system is incredible. The tutorial is great, and the documentation available for anything I could want to do is somewhere in C-h. Apropos is nice; having explainers for each key combo with C-h k; it’s all fantastic.
  • Emacs’ built-in shell works really nicely. (Vim gained a terminal in 8.1, but I never got along too well with it.) There are lots of integrations that work really nicely. I was particularly impressed when git asked for an SSH key passphrase, and Emacs prompted me in the minibuffer for the passphrase so that it wouldn’t be echoed in the terminal.
  • In addition to being a very capable terminal editor, it’s also nicely integrated with the desktop. Copy/paste works fine with Wayland, and the mouse works nicely, and having a list of menus and icons, while I don’t use them heavily, is rather convenient.

I’ll note, to be fair to Vim, that I was never quite this intentional about learning its features. Most everything I’m able to do this far, I was also able to do in Vim, or would have been able to figure out how to, had I wanted.

Soundtrack

Night Train, by the Oscar Peterson Trio Severe Tire Damage, by They Might Be Giants The Golden Hour, by Dave Koz and Cory Wong2 Aretha in Person with The Ray Bryant Combo, by Aretha Franklin Twenty Dozen, by the Dirty Dozen Brass Band Sound of Christmas, by the Ramsey Lewis Trio


  1. Maybe I’m just becoming an old grouch, but having not had syntax highlighting for much of the week, I honestly don’t miss it as much as I thought I would. I often find it useful for finding trivial syntax errors (“I didn’t close a quote” being a common one), and I suppose to some extent as an additional visual cue for navigation, but I didn’t really find either of those to be significantly impeding here. ↩︎

  2. Cory Wong is on Bandcamp! Here’s The Golden Hour. I 10/10 recommend both Cory Wong and Bandcamp: wicked awesome guitar, and DRM-free no-nonsense FLAC music downloads, respectively. ↩︎


I don't have a formal commenting system set up. If you have questions or comments about anything I've written, send me an email and I'd be delighted to hear what you have to say!