3
\$\begingroup\$

Using Advent of Code as a source of toy problems for my first dabblings in Rust. Looking for feedback from more-seasoned Rustaceans on usage of the language.

Advent of Code Day 1: Inverse Captcha

use std::fs::File;
use std::io::prelude::*;

fn get_input() -> Vec<u8> {
    let mut file = File::open("input.txt").unwrap();
    let mut content = Vec::new();

    file.read_to_end(&mut content).unwrap();
    for i in 0 .. content.len() {
        content[i] = content[i] - ('0' as u8);
    }

    content
}

fn offset_sum(input: &[u8], offset: usize) -> i32 {
    let len = input.len();
    let mut sum = 0;

    for i in 0 .. len {
        let j = (i + offset) % len;
        if input[i] == input[j] {
            sum += input[i] as i32;
        }
    }

    sum
}

fn main() {
    let input = get_input();

    println!("Part 1 total: {}", offset_sum(&input, 1));
    println!("Part 2 total: {}", offset_sum(&input, input.len()/2));
}
\$\endgroup\$

3 Answers 3

4
\$\begingroup\$

To be honest, for a simple, one-off problem, I disagree that hardcoding an input file name is disadvantageous. Similarly for error handling; you know the file is there and will read fine, so unwrapping it is fine; if it fails for some stupid reason there's no recovery you can do anyway.

And because the problem is so simple, there's not much to go about complaining about. This is a perfectly fine solution, because it's very readable.

But there are a few places where the standard library could be better utilized: (Note: I may go a little far here. Go as far iterator as you feel comfortable.)

The iteration over content's indices in get_input can be replaced by a mutating iterator:

for byte in &mut content {
    *byte -= b'0';
}

Here I've also used a byte character literal, rather than casting to u8, which clippy will warn you about. With a known constant, it's safe, but then you should just use the byte literal.

What this says is to, for each position in content, take the thing at the position *byte, and decrement (-=) it by b'0'. It's the exact same as what you did, except without the manual indexing. (It's probably lowered to the same machine code in the end, but iterators are fun!)

offset_sum provides a more interesting field for refactoring, though. It can be done in one iterator chain by cleverly utilizing the available tools.

input

Start with the slice,

    .iter()

and get an iterator from it.

    .cloned()

Since we're operating over a Copy type, I dereference it here for convenience. If we just call iter, we get an iterator over &u8, but calling cloned clones each element, which in this case is just a cheap Copy. Don't go doing this with non-copy types, though; this is just a convenience.

    .enumerate()

We pair up each byte with its index in the iterator.

    .filter(|&(i, b)| b == input[(i + offset) % input.len()])

Filter out all of the numbers that don't match our requirements.

    .map(|(_, b)| u32::from(b))

Map our iterator back to just the number we care about. Convert it to a u32 so that we can

    .sum()

it into a u32 and consume the iterator.

Try it Online!

Some fun extensions you might try:

This is always going to be an \$O(n)\$ algorithm because you have to scan the whole string. But rather than handle indices, in the first half you could use windows to grab each neighboring pair of bytes, though you will have to handle wraparound somehow. In the second half, you can reduce the number of comparisons you have to do by half, because each number is compared against the same number in either direction (the absolute opposite of a circular buffer). Maybe you can split the slice into two halves, then zip the two halves together for comparisons?

\$\endgroup\$
3
\$\begingroup\$

I haven't ever used Rust, but this is irrespective of language choice:

Don't hardcode the name of the input file in the program. More conventional, and flexible, ways for providing the input are:

  1. Read the input directly from stdin
  2. Take the input as command line argument
  3. Take the name of the input file as command line argument

Even when using the first two options, you can still use a file to store the input:

$ ./program <input.txt
$ ./program $(cat input.txt)
\$\endgroup\$
1
  • \$\begingroup\$ That won't work in some cases. For example, this input for day 1 of 2018. The number of lines in the input is not provided, so there is no way to know when to stop reading stdin. \$\endgroup\$
    – Delgan
    Dec 1, 2018 at 8:39
0
\$\begingroup\$

If you only need to read one line from a file that will not change at runtime, you can use include_str!

As already said, the whole program can be simplified a lot using a functional programming style on iterators:

fn result(s: &str) -> u32 {
    s.chars()
     .zip(s.chars().cycle().skip(1))
     .filter(|&(a, b)| a == b)
     .filter_map(|(a, _)| a.to_digit(10))
     .sum()
}

fn main() {
    // is everything ok?
    assert_eq!(result("1122"), 3);
    assert_eq!(result("1111"), 4);
    assert_eq!(result("1234"), 0);
    assert_eq!(result("91212129"), 9);

    // get your answer:
    println!("{}", result(include_str!("input.txt")));
}

Explanations:

s.chars().cycle().skip(1) gives you a shifted string. cycle makes the iterator cycle infinitely, skip(1) remove the first (replace it with cycle(s.len() / 2) for the second part.

You the zip the two iterators: one on the string, one on the shifted string.

Then you keep the pairs with same numbers: filter(|&(a, b)| a == b)

Then you convert each char to u32 and you discard the failed conversions.

Then you make the sum of all the numbers.

If you want to do the two days in one like in your code:

fn offset_sum(s: &str, day1: bool) -> u32 {

    fn get_sum(s: &str, offset: usize) -> u32 {
        s.chars()
         .zip(s.chars().cycle().skip(offset))
         .filter(|&(a, b)| a == b)
         .filter_map(|(a, _)| a.to_digit(10))
         .sum()
    }

    let offset = if day1 { 1 } else { s.len() / 2 };
    get_sum(s, offset)
}

fn main() {
    let input = include_str!("input.txt");

    println!("day 1: {}", offset_sum(input, true));
    println!("day 2: {}", offset_sum(input, false));
}
\$\endgroup\$

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.