Day 4: Camp Cleanup

Elves have been paired up to clean up the base camp before we head out on the star fruit expedition. Quite a few have been assigned redundant tasks with one of the pair fully or partially overlapping with the other. For example the sample input:

2-4,6-8           | No overlap
2-3,4-5           | No overlap
5-7,7-9           | 7 overlaps
2-8,3-7           | 3-7 is entirely within 2-8
6-6,4-6           | 6 is entirely within 4-6
2-6,4-8           | 4-6 overlap

Parsing the input

First some type aliases to help communicate intent:

type Range = (u32, u32);
type Pair = (Range, Range);

I’ve yet to find a performant regex library for Rust, so I’ll use the , to split the pairs and - to split the range bounds.

fn parse_input(input: &String) -> Vec<Pair> {
    input.lines().map(parse_line).collect()
}

fn parse_line(line: &str) -> Pair {
    let parts: Vec<&str> = line.split(',').collect();
    (
        parse_range(parts[0]),
        parse_range(parts[1])
    )
}

fn parse_range(spec: &str) -> Range {
    let limits: Vec<u32> =
        spec.split('-')
            .map(|str| str.parse::<u32>().unwrap())
            .collect();

    (limits[0], limits[1])
}
// ...
fn sample_pairs() -> Vec<Pair> {
    vec![
        ((2, 4), (6, 8)),
        ((2, 3), (4, 5)),
        ((5, 7), (7, 9)),
        ((2, 8), (3, 7)),
        ((6, 6), (4, 6)),
        ((2, 6), (4, 8)),
    ]
}

#[test]
fn can_parse() {
    let input = "2-4,6-8
2-3,4-5
5-7,7-9
2-8,3-7
6-6,4-6
2-6,4-8".to_string();

    assert_eq!(parse_input(&input), sample_pairs());
}

Part 1 - Wholly redundant

First task, I have to count the pairs where one elf is wholly redundant, e.g. lines 4 and 5 in the sample. The hard work was all done when parsing the input, I now need to filter to only the pairs where one elf starts on or before the other, and finishes on or after the other.

fn count_redundant_pairs(pairs: &Vec<Pair>) -> usize {
    pairs.iter().filter(|&&pair| pair_has_redundant_elf(pair)).count()
}

fn pair_has_redundant_elf(
    ((elf1_start, elf1_end), (elf2_start, elf2_end)): Pair
) -> bool {
    (elf1_start <= elf2_start && elf1_end >= elf2_end) ||
        (elf1_start >= elf2_start && elf1_end <= elf2_end)
}

The sample data can also be used as a test cases.

#[test]
fn can_find_redundant_pairs() {
    for (pair, expected) 
    in sample_pairs().into_iter().zip(vec![false, false, false, true, true, false]) 
    {
        assert_eq!(
            pair_has_redundant_elf(pair), 
            expected, 
            "Check pair {:?} redundancy", pair
        );
    }
    
    let boundaries = vec![
        ((4, 4), (4, 6)),
        ((6, 6), (4, 6)),
        ((4, 6), (4, 4)),
        ((4, 6), (6, 6)),
    ];
    
    for pair in boundaries {
        assert_eq!(
            pair_has_redundant_elf(pair), 
            true, 
            "Check pair {:?} redundancy", pair
        );
    }
}

#[test]
fn can_count_pairs() {
    assert_eq!(count_redundant_pairs(&sample_pairs()), 2);
}

With those passing I can apply count_redundant_pairs to the puzzle input.

pub fn run() {
    let contents = 
        fs::read_to_string("res/day-4-input").expect("Failed to read file");
    let pairs = parse_input(&contents);

    println!(
        "There are {} redundant pairs of elves",
        count_redundant_pairs(&pairs)
    );
}

Part 2 - Some overlap

Part two is very similar to part 1, but has a more lenient filter. I need to count the pairs that have any overlap.

First a quick refactor to extract the filter to a parameter of the count method.

fn count_pairs_matching(pairs: &Vec<Pair>, predicate: fn(Pair) -> bool) -> usize {
    pairs.iter().filter(|&&pair| predicate(pair)).count()
}
// ...
#[test]
fn can_count_pairs() {
    assert_eq!(count_pairs_matching(&sample_pairs(), pair_has_redundant_elf), 2);
}
// ...
pub fn run() {
    let contents = 
        fs::read_to_string("res/day-4-input").expect("Failed to read file");
    let pairs = parse_input(&contents);

    println!(
        "There are {} redundant pairs of elves",
        count_pairs_matching(&pairs, pair_has_redundant_elf)
    );
}
// There are 513 redundant pairs of elves 

Now I can add and test the more lenient version of the predicate. The sample data doesn’t cover the full spectrum of edge cases, so I wrote some extra.

fn pair_overlaps(((elf1_start, elf1_end), (elf2_start, elf2_end)): Pair) -> bool {
    elf1_start <= elf2_end && elf1_end >= elf2_start
}
// ...
#[test]
fn can_find_overlaps_pairs() {
    let possibilities = vec![
        (((4, 6), (5, 5)), true),
        (((5, 5), (4, 6)), true),
        (((4, 5), (5, 6)), true),
        (((5, 6), (4, 5)), true),
        (((4, 5), (6, 7)), false),
        (((6, 7), (4, 5)), false),
    ];
    for (pair, expected) in possibilities {
        assert_eq!(pair_overlaps(pair), expected, "Check pair {:?} overlap", pair);
    }
}

This passing, I can plug the second predicate into the count function test and the run method.

#[test]
fn can_count_pairs() {
    assert_eq!(count_pairs_matching(&sample_pairs(), pair_has_redundant_elf), 2);
    assert_eq!(count_pairs_matching(&sample_pairs(), pair_overlaps), 4);
}
// ...
pub fn run() {
    let contents = 
        fs::read_to_string("res/day-4-input").expect("Failed to read file");
    let pairs = parse_input(&contents);

    println!(
        "There are {} redundant pairs of elves",
        count_pairs_matching(&pairs, pair_has_redundant_elf)
    );

    println!(
        "There are {} overlapping pairs of elves",
        count_pairs_matching(&pairs, pair_overlaps)
    );
}
// There are 513 redundant pairs of elves  
// There are 878 overlapping pairs of elves
// 
// Finished in 2.76ms