median blog

Advent of Code 2024 Day 2 - Rust

Part One

On day two, we're checking lists of numbers against two rules. First, all numbers need to increase or decrease. Second, the difference between each number and the next must be between one and three.

I set up an enum to store the state as I parsed each row. Each row was initially in an Unknown state and became either Increasing or Decreasing after reading the first two values. I kept track of the current value in the list as I parsed it and stored the previous value to check against.

enum LevelState {
    Unknown,
    Increasing,
    Decreasing,
}

fn day2() {
    let content = fs::read_to_string("./inputs/day2.txt").expect("Couldn't read input");

    let mut safe = 0;

    for line in content.lines() {
        let mut state: LevelState = LevelState::Unknown;
        let mut previous: Option<i32> = None;
        safe += 1;

I split each line at each space character and parsed the elements into integers. Then, based on whether I had already established a direction (Increasing or Decreasing), I checked that the most recent element continued the trend. If it did, I also checked the difference between one and three.

        for c in line.split(" ") {
            let val: i32 = c.parse().unwrap();
            match state {
                LevelState::Unknown => {
                    if let Some(x) = previous {
                        if val > x {
                            state = LevelState::Increasing;
                        } else if val < x {
                            state = LevelState::Decreasing;
                        } else {
                            safe -= 1;
                            break; // If they match
                        }
                    }
                }
                LevelState::Increasing => {
                    if let Some(x) = previous {
                        if val <= x {
                            safe -= 1;
                            break;
                        }
                    }
                }
                LevelState::Decreasing => {
                    if let Some(x) = previous {
                        if val >= x {
                            safe -= 1;
                            break;
                        }
                    }
                }
            }

            if let Some(x) = previous {
                if (val - x).abs() > 3 {
                    safe -= 1;
                    break;
                }
            }

            previous = Some(val);
        }

This got me the answer for part one. It felt a little clunky, and I knew already that I would regret writing it all in one large function, but in the spirit of YAGNI, I let it be.

Part 2

Part 2 introduced an additional rule: each line may contain one incorrect element. It was also considered safe if the line met the rules with one element removed.

I moved the checking logic into its own function. The function returned either a Match or a FailAt(usize) indicating the match failed and which element caused the issue.

enum CheckState {
    Match,
    FailAt(usize),
}

fn check(items: Vec<i32>) -> CheckState {
    let mut state: LevelState = LevelState::Unknown;
    let mut new_state = LevelState::Unknown;
    let mut previous: Option<i32> = None;

    for (i, val) in items.iter().enumerate() {
        match state {
            LevelState::Unknown => {
                if let Some(x) = previous {
                    if *val > x {
                        new_state = LevelState::Increasing;
                    } else if *val < x {
                        new_state = LevelState::Decreasing;
                    } else {
                        return CheckState::FailAt(i);
                    }
                }
            }
            LevelState::Increasing => {
                if let Some(x) = previous {
                    if *val <= x {
                        return CheckState::FailAt(i);
                    }
                }
            }
            LevelState::Decreasing => {
                if let Some(x) = previous {
                    if *val >= x {
                        return CheckState::FailAt(i);
                    }
                }
            }
        }

        if let Some(x) = previous {
            if (*val - x).abs() > 3 {
                return CheckState::FailAt(i);
            }
        }

        previous = Some(*val);
        state = new_state.clone();
    }

    return CheckState::Match;
}

The intent was to recheck the line, removing each possible element that could have caused the mismatch. While I am reasonably sure this would have led to a more elegant solution, I was finishing this right before going to bed, so I opted for the lazier option of "if the line fails, loop through each element, remove it and try that" instead.

    for line in content.lines() {
        let items: Vec<i32> = line.split(" ").map(|x| x.parse().unwrap()).collect();
        match check(items.clone()) {
            CheckState::Match => {
                safe += 1;
            }
            CheckState::FailAt(i) => {
                for a in 0..items.len() {
                    let mut rest = items.clone();
                    rest.remove(a);
                    if let CheckState::Match = check(rest) {
                        safe += 1;
                        break;
                    }
                }
            }
        }

It's not my finest work, but it was enough to nab that second gold star.

The complete solution can be found here.