2
\$\begingroup\$

I'm preparing for the Advent of Code 2022, and in order to shake off the cobwebs, I'm attempting some problems from the previous year in rust. Starting at Day 1, I've been trying to get comfortable with iterator mechanics. The problem description for both parts is:

Suppose you had the following report:

199 200 208 210 200 207 240 269 260 263

This report indicates that, scanning outward from the submarine, the sonar sweep found depths of 199, 200, 208, 210, and so on.

The first order of business is to figure out how quickly the depth increases, just so you know what you're dealing with - you never know if the keys will get carried into deeper water by an ocean current or a fish or something.

To do this, count the number of times a depth measurement increases from the previous measurement. (There is no measurement before the first measurement.) In the example above, the changes are as follows:

199 (N/A - no previous measurement) 200 (increased) 208 (increased) 210 (increased) 200 (decreased) 207 (increased) 240 (increased) 269 (increased) 260 (decreased) 263 (increased) In this example, there are 7 measurements that are larger than the previous measurement.

How many measurements are larger than the previous measurement?

Part 2: Instead, consider sums of a three-measurement sliding window. Again considering the above example:

199 A 200 A B 208 A B C 210 B C D 200 E C D 207 E F D 240 E F G 269 F G H 260 G H 263 H Start by comparing the first and second three-measurement windows. The measurements in the first window are marked A (199, 200, 208); their sum is 199 + 200 + 208 = 607. The second window is marked B (200, 208, 210); its sum is 618. The sum of measurements in the second window is larger than the sum of the first, so this first comparison increased.

Your goal now is to count the number of times the sum of measurements in this sliding window increases from the previous sum. So, compare A with B, then compare B with C, then C with D, and so on. Stop when there aren't enough measurements left to create a new three-measurement sum.

In the above example, the sum of each three-measurement window is as follows:

A: 607 (N/A - no previous sum) B: 618 (increased) C: 618 (no change) D: 617 (decreased) E: 647 (increased) F: 716 (increased) G: 769 (increased) H: 792 (increased) In this example, there are 5 sums that are larger than the previous sum.

Consider sums of a three-measurement sliding window. How many sums are larger than the previous sum?

My solution for both parts:

// main.rs
use std::fs::File;
use std::io::{Result, Read};


fn parse_to_int(body: String) -> Vec<i32> {
    // Parse the file once and collect into a vec
    body
        .split("\n")
        .map(|x| x.trim().parse::<i32>().unwrap())
        .collect()
}


fn part_one(parsed: &Vec<i32>) -> i32 {
    // offset iterator to compare if parsed[i] < parsed[i+1]
    // sum all occurrences where this is true
    parsed
        .iter()
        .zip(parsed.iter().skip(1))
        .map(|(a, b)| (b > a) as i32)
        .sum()
}


fn part_two(parsed: &Vec<i32>) -> i32 {
    // Need a sliding window of three elements wide
    let a = parsed
        .iter()
        .zip(parsed.iter().skip(1))
        .zip(parsed.iter().skip(2))
        .map(|((x, y), z)| x + y + z);

    // I tried doing let b = a.skip(1);
    // but the borrow checker wouldn't have it.
    // This works though
    let b = parsed
        .iter()
        .skip(1)
        .zip(parsed.iter().skip(2))
        .zip(parsed.iter().skip(3))
        .map(|((x, y), z)| x + y + z);

    a.zip(b)
        .map(|(x, y)| (y > x) as i32)
        .sum()
}



pub fn main() -> Result<()> {
    let mut fh = File::open("data/day1.txt")?;
    let mut body = String::new();

    fh.read_to_string(&mut body)?;

    body = body.trim().to_string();

    let values = parse_to_int(body);

    let total_1 = part_one(&values);

    println!("Part 1 solution: {:?}", total_1);

    let total_2 = part_two(&values);

    println!("Part 2 solution: {:?}", total_2);

    Ok(())
}


#[cfg(test)]
#[test]
fn test_int_conversion() {
    // Make sure my integer converter works
    let test = String::from("199\n200\n208\n210\n200\n207");

    let res = parse_to_int(test);

    assert_eq!(
        res,
        vec![199, 200, 208, 210, 200, 207]
    );
}

#[cfg(test)]
#[test]
fn test_part_one() {
    let test = String::from(
        "199\n200\n208\n210\n200\n207\n240\n269\n260\n263"
    );
    let res = parse_to_int(test);

    let tot = part_one(&res);

    assert_eq!(tot, 7);
}


#[cfg(test)]
#[test]
fn test_part_two() {
    let test = String::from(
        "199\n200\n208\n210\n200\n207\n240\n269\n260\n263"
    );
    let res = parse_to_int(test);

    let tot = part_two(&res);

    assert_eq!(tot, 5);
}

Any advice is welcome, but I'd love some pointers on if I can make this more idiomatic. As a note, the body.strip() line is due to the fact that my editor keeps putting an extra newline at the end of the AoC data file, I shouldn't need it otherwise.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Your code looks good to me! Here is how we can try to make things better.

Use more tools: fmt

Your code looks properly formatted. However, for your information, you can use cargo fmt to ensure that you never have to worry about formatting your code manually anymore.

With the code provided, the only changes performed are about blank lines and lines break.

Use more tools: clippy

clippy is a really nice tool to catch mistakes and improve your code.

In your case, there is nothing spectacular suggested but it is worth adding it to your toolbox.

Here are the suggestions, all of them are easy to take into account.

error: this argument is passed by value, but not consumed in the function body
 --> src/2021/day1/main_review.rs:6:23
  |
6 | fn parse_to_int(body: String) -> Vec<i32> {
  |                       ^^^^^^ help: consider changing the type to: `&str`
  |
  = note: `-D clippy::needless-pass-by-value` implied by `-D clippy::pedantic`
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#needless_pass_by_value
error: single-character string constant used as pattern
 --> src/2021/day1/main_review.rs:9:16
  |
9 |         .split("\n")
  |                ^^^^ help: try using a `char` instead: `'\n'`
  |
  = note: `-D clippy::single-char-pattern` implied by `-D clippy::perf`
  = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#single_char_pattern
error: writing `&Vec` instead of `&[_]` involves a new object where a slice will do
  --> src/2021/day1/main_review.rs:12:21
   |
12 | fn part_one(parsed: &Vec<i32>) -> i32 {
   |                     ^^^^^^^^^ help: change this to: `&[i32]`
   |
   = note: `-D clippy::ptr-arg` implied by `-D clippy::style`
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#ptr_arg
error: casting `bool` to `i32` is more cleanly stated with `i32::from(_)`
  --> src/2021/day1/main_review.rs:18:23
   |
18 |         .map(|(a, b)| (b > a) as i32)
   |                       ^^^^^^^^^^^^^^ help: try: `i32::from(b > a)`
   |
   = note: `-D clippy::cast-lossless` implied by `-D clippy::pedantic`
   = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#cast_lossless

Simplifying tests

In particular, once you've taken these suggestions into account, you can get rid of the String::from in your test cases and get something much more concise:

#[cfg(test)]
#[test]
fn test_int_conversion() {
    let res = parse_to_int("199\n200\n208\n210\n200\n207\n240\n269\n260\n263");
    assert_eq!(res, vec![199, 200, 208, 210, 200, 207, 240, 269, 260, 263]);
}

#[cfg(test)]
#[test]
fn test_part_one() {
    let res = parse_to_int("199\n200\n208\n210\n200\n207\n240\n269\n260\n263");
    let tot = part_one(&res);
    assert_eq!(tot, 7);
}

#[cfg(test)]
#[test]
fn test_part_two() {
    let res = parse_to_int("199\n200\n208\n210\n200\n207\n240\n269\n260\n263");
    let tot = part_two(&res);
    assert_eq!(tot, 5);
}

You can also try to define the input example just once (and maybe remove various variables used just once):

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

    const INPUT_EXAMPLE: &str = "199\n200\n208\n210\n200\n207\n240\n269\n260\n263";

    #[cfg(test)]
    #[test]
    fn test_int_conversion() {
        assert_eq!(
            parse_to_int(INPUT_EXAMPLE),
            vec![199, 200, 208, 210, 200, 207, 240, 269, 260, 263]
        );
    }

    #[test]
    fn test_part_one() {
        assert_eq!(part_one(&parse_to_int(INPUT_EXAMPLE)), 7);
    }

    #[test]
    fn test_part_two() {
        assert_eq!(part_two(&parse_to_int(INPUT_EXAMPLE)), 5);
    }
}

Solution for part 1

The algorithm put in place for part 1 looks good to me.

However, instead of having parsed.iter() multiple times including one shifted of 1, you could directly yuse tuple_windows (from the itertools crate) to write: parsed.iter().tuple_windows().map(|(a, b)| i32::from(b > a)).sum().

This may look a bit overkilled but this will be even more relevant for the part 2.

Also, the variable name parsed does not convey much meaning. My preference would go for something like depths (even though it is easy to forget that we are tackling a submarine problem and not just a mathematical one).

As a final side note, instead of having map(|(a, b)| i32::from(b > a)).sum(), one could use filter(|(a, b)| a < b).count() but I cannot guarantee that it is more efficient and/or more idiomatic.

Solution for part 2

For this particular part, a mathematical trick can be applied. Indeed, we are comparing sum of sliding windows but doing it the obvious way: compute sliding windows, compute sums, compare sums. Instead, the following observation can be done:

Computing:

  • Window(n + 1) > Window(n)

ss the same as computing:

  • d(n+1) + d(n+2) + ... + d(n+1+windowsize) > d(n) + d(n+1) + ... + d(n+windowsize) (where d is depth)

Which is the same as computing:

  • d(n+1+windowsize) > d(n) which does not involve any kind of summation.

Hence, the solution for part 2 can be written:

fn part_two(depths: &[i32]) -> usize {
    // Window(n + 1) > Window(n)
    // d(n+1) + d(n+2) + ... + d(n+1+windowsize) > d(n) + d(n+1) + ... + d(n+windowsize)
    // d(n+1+windowsize) > d(n)
    depths
        .iter()
        .tuple_windows()
        .filter(|(a, _, _, d)| a < d)
        .count()
}
\$\endgroup\$
1
  • 1
    \$\begingroup\$ I will say that I think the filter(|(a, b)| a < b).count() is certainly much more readable. I'll have to benchmark the two approaches to see which one is faster \$\endgroup\$
    – C.Nivs
    Oct 21, 2022 at 14:18

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.