Day 2 - 99 green bottles sitting on a wall

Created on Monday, December 2, 2024.

Day 2

Ok, so we're reading rows of data, each with 5 sequential levels, each one a report card. We have a number of tests, called predicates, that we can apply to the sequence of numbers that tells us whether the report is valid or not.

Ah ha, doing something I sometimes forget to do, the actual data has a varying number of levels, not a fixed number, so we'll need to think about that I suspect that means that we have a Vector of Vectors of u32's, so each are variable length.

We then need to filter the list based on a predicate that can be ascertained from reviewing the list. The second predicate, any two adjacant level differs by at least one and at most three is easy to check, as the predicate relies on no outside knowledge

But the predicate that says levels are either all increasing or all decreasing requires either keeping state from item to item, or running over the list twice.

My gut feel is to do a check to mark as unsafe if "direction changes" or if the change is outside of 1..3,

Let's start with testing whether a sequence matches the first predicate, whether they differ by more than 1 to 3 in any given change.

pub fn high_varience(report: &Vec<i32>) -> bool {
    // Filter it down if
    report
        .windows(2) // For each pair of values
        .all(
            // They all...
            |pair| {
                let diff = pair[0].abs_diff(pair[1]);
                diff >= 1 && diff <= 3 // Have a difference of 1-3
            },
        )
}

By making this a function that takes a sequence and returns a boolean, we can use it in a filter statement later easily. It also makes it really easy to test sequences of numbers.

The windows function creates an iterator that builds up a small sliding window and passes that to the next function, so windows(2) on [1 2 3 4 5] would result in [1 2], [2, 3], [3, 4], [4, 5]. It nicely handles everything you need for this kind of test. The all function returns true, if and only if every item in the sequence returns true from the function. Between them, we get a true if every pair of the sequence is between 1 and 3 steps away from each other.

pub fn rise_or_fall(report: &Vec<i32>) -> bool {
    report // For each report
        .windows(2) // For each pair of values
        .fold(0, |dir, pair| {
            if pair[0] < pair[1] && (dir == 0 || dir == 1) {
                1
            }
            // If the last pair was increasing or the start
            else if pair[0] > pair[1] && (dir == 0 || dir == -1) {
                -1
            }
            // Of if the last pair was decreasing or the start
            else {
                -99
            } // Otherwise signal an error
        })
        != -99 // Check for any errors
}

This on the other hand is ugly as sin. We use the same windows method, but now we're going to fold left. A fold left for function f is like called f(f(f(f(f,[1,2]),[2,3]),[3,4]),[4,5]). That's to say, it calls the function for the first pair, and passes the result to the next function on the next pair as the first variable. I've used 1 to indicate that the numbers increase in a pair, and -1 to indicate they go down. I've used 0 to begin with for no value, and -99 for an error.

The reason for the -99 is for error propogation. If we just reported whether a pair increased or decreased, it we'd only get the result of the last pair from the function. Instead we want to know whether the numbers are increasing and were in all the previous pairs, or decreasing and were in all the previous pairs. If it's ever not the case, we pass a -99, and every test after that will fail because the direction parameter will be neither 1 or -1.

Now I write this up (days later), I realise that this would have been a perfect use case for a simple enum, and an Option, which is a special variable that is either Some(x) where x has a value, or None. The None's propogate in exactly this fashion.

But not to be helped now, this worked, and onto part 2.

Part 2 - And if one green bottle should accidentally fall...

Now we learn that the safety system is safer than we thought and can handle errors. If there's a single digit that fails one of our tests, then we are able to continue.

I note from my comments that originaly, I think these predicates were inlined, so I refactored them out as part of the part 2, to make them reusable.

Something intersting in this approach is that we're talking about literally ignoring a number, not tolerating an error. So for the rising, [1 2 1 3 4] is a good example where if we accept an error in dropping from 2 to 1, we can still either compare 2 -> 3 (dropping the 1), or 1 -> 3 (tolerating the error).

In the case of tolerating the error, we'd have had to use the fold left approach to propogate whether we'd seen an error anywhere in the predicate. But in this case, what we actually want to test, is whether any permutation of the set with a number dropped is still valid.

So set [1 2 3] has permutations ([1,2], [1,3], [2,3]) and so on.

I didn't realise that there was a permutations function in the itertools cargo, so I wrote my own for this specific example:

pub fn permutate_list(input: &Vec<i32>) -> Vec<Vec<i32>> {
    let mut v: Vec<Vec<i32>> = Vec::new();
    for (index, _) in input.iter().enumerate() {
        let mut n = input.clone();
        n.remove(index);
        v.push(n);
    }
    return v;
}

Given a list of arbitrary length it returns a list of lists, where each sublist has a different number removed from teh original list.

We then filter where any of the sub lists pass the tests, and count the totals and bob is your uncle!

Next

Day 3 - Regular expressions

Previous

Day 1 - Pair and sum boys, pair and sum