Learning Rust EP.2
Learning Rust EP.2
Welcome back! Today I decided to exploit Advent of Code to learn Rust. At the moment of writing, I have completed days 1 and 2.
Advent of Code 2024 - Day 1
The first day is pretty straightforward, we have to find the distance between two lists (which had to be sorted before).
The important part of this day is the setup.
My setup is the following (pretty much inspired by this repository):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
> advent_of_code
> day-01
> src
> bin
> part1.rs
> part2.rs
> lib.rs
> part1.rs
> part2.rs
> Cargo.toml
> [...]
> res
> input.txt
> Cargo.toml
> Cargo.lock
> justfile
There is a lot of stuff. Let’s break it down:
day-01
is the folder for the first daysrc
contains the source codebin
contains the .rs files that run the actual codelib.rs
is the library filepart1.rs
andpart2.rs
are the source code for the two partsCargo.toml
is the configuration file for the projectres
contains the input filejustfile
is a file to run the code
During this AoC, I decided to use justfiles. Just is a command runner that allows you to define tasks in a file. It’s pretty useful to run the code and test it.
My justfile
is the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
set dotenv-load
# Use `just work day-01 part1` to work on the specific binary for a specific day's problems
work day part:
cargo watch -w -x "check -p " -s "just test " -s "just lint "
lint day:
cargo clippy -p
test day part:
cargo nextest run -p
bench-all:
cargo bench -q > benchmarks.txt
bench day part:
cargo bench --bench -bench >> .bench.txt
# create the directory for a new day's puzzle and fetch the input
create day:
cargo generate --path ./daily-template --name
just get-input
get-input day:
./scripts/get-aoc-input.rs --day --current-working-directory
The work
task is the most important one. It watches the day
folder and runs the check
, test
and lint
tasks.
My solution for the first day is the following:
Part 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub fn process(input: &str) -> miette::Result<String> {
let mut left = vec![];
let mut right = vec![];
for line in input.lines() {
let mut items = line.split_whitespace();
left.push(items.next().unwrap().parse::<i32>().unwrap());
right.push(items.next().unwrap().parse::<i32>().unwrap());
}
left.sort();
right.sort();
let result: i32 = std::iter::zip(left, right)
.map(|(l, r)| (l - r).abs())
.sum();
Ok(result.to_string())
}
Part 2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pub fn process(input: &str) -> miette::Result<String> {
let mut left = vec![];
let mut right = vec![];
for line in input.lines() {
let mut items = line.split_whitespace();
left.push(
items.next().unwrap().parse::<usize>().unwrap(),
);
right.push(
items.next().unwrap().parse::<usize>().unwrap(),
);
}
let result: usize = left
.iter()
.map(|number| {
number
* right
.iter()
.filter(|r| &number == r)
.count()
})
.sum();
Ok(result.to_string())
}
Advent of Code 2024 - Day 2
The second day is a bit more complicated. We have to which sequence of numbers is “safe” or “unsafe” based on the previous numbers.
It was an occasion to learn about Enumerators
and Pattern Matching
.
In this case, the goal was to define an enumerator for Direction
. I did it this way:
1
2
3
4
enum Direction {
Increasing,
Decreasing,
}
Later in the code, I will use this enumerator to check if the sequence is increasing or decreasing.
Let’s see my solution for the second day:
Part 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
use itertools::Itertools;
use miette::miette;
use nom::character::complete;
use nom::character::complete::{line_ending, space1};
use nom::multi::separated_list1;
use nom::IResult;
use tracing::instrument;
enum Direction {
Increasing,
Decreasing,
}
#[tracing::instrument]
pub fn process(_input: &str) -> miette::Result<String> {
let (_, reports) = parse(_input).map_err(|e| miette!("parse failed {}", e))?;
let result = reports
.iter()
.filter(|report| check_safety(report).is_ok())
.count();
Ok(result.to_string())
}
#[instrument(ret)]
fn check_safety(report: &Report) -> Result<(), String> {
let mut direction: Option<Direction> = None;
for (a, b) in report.iter().tuple_windows() {
let diff = b - a; // Assuming we calculate `b - a` for direction.
let diff_abs = diff.abs();
if !(1..=3).contains(&diff_abs) {
return Err(format!(
"Invalid difference between {} and {}: {}",
a, b, diff_abs
));
}
match diff.signum() {
1 => match direction {
Some(Direction::Decreasing) => {
return Err(format!(
"Direction switched to increasing at {} and {}",
a, b
));
}
_ => direction = Some(Direction::Increasing),
},
-1 => match direction {
Some(Direction::Increasing) => {
return Err(format!(
"Direction switched to decreasing at {} and {}",
a, b
));
}
_ => direction = Some(Direction::Decreasing),
},
0 => {
return Err(format!("No difference (0) between {} and {}", a, b));
}
_ => {
return Err(format!(
"Unexpected difference signum between {} and {}",
a, b
));
}
}
}
Ok(())
}
type Report = Vec<i32>;
fn parse(_input: &str) -> IResult<&str, Vec<Report>> {
separated_list1(line_ending, separated_list1(space1, complete::i32))(_input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9";
assert_eq!("2", process(input)?);
Ok(())
}
}
As you can see, I used nom
to parse the input. I also used itertools
to iterate over the report and check the safety of the sequence.
I also used miette
to handle errors. Furthermore, I found it pretty useful and interesting.
Part 2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
use itertools::Itertools;
use miette::miette;
use nom::character::complete;
use nom::character::complete::{line_ending, space1};
use nom::multi::separated_list1;
use nom::IResult;
use tracing::instrument;
enum Direction {
Increasing,
Decreasing,
}
#[tracing::instrument]
pub fn process(_input: &str) -> miette::Result<String> {
let (_, reports) = parse(_input).map_err(|e| miette!("parse failed {}", e))?;
let result = reports
.iter()
.filter(|report| make_safe(report.to_vec()).is_some()) // Now filtering reports that can be made safe
.count();
Ok(result.to_string())
}
#[instrument(ret)]
fn check_safety(report: &Report) -> Result<(), String> {
let mut direction: Option<Direction> = None;
for (a, b) in report.iter().tuple_windows() {
let diff = b - a; // Assuming we calculate `b - a` for direction.
let diff_abs = diff.abs();
if !(1..=3).contains(&diff_abs) {
return Err(format!(
"Invalid difference between {} and {}: {}",
a, b, diff_abs
));
}
match diff.signum() {
1 => match direction {
Some(Direction::Decreasing) => {
return Err(format!(
"Direction switched to increasing at {} and {}",
a, b
));
}
_ => direction = Some(Direction::Increasing),
},
-1 => match direction {
Some(Direction::Increasing) => {
return Err(format!(
"Direction switched to decreasing at {} and {}",
a, b
));
}
_ => direction = Some(Direction::Decreasing),
},
0 => {
return Err(format!("No difference (0) between {} and {}", a, b));
}
_ => {
return Err(format!(
"Unexpected difference signum between {} and {}",
a, b
));
}
}
}
Ok(())
}
#[instrument(ret)]
fn make_safe(report: Report) -> Option<Report> {
for i in 0..report.len() {
let mut modified = report.clone();
modified.remove(i);
if check_safety(&modified).is_ok() {
return Some(modified);
}
}
None
}
type Report = Vec<i32>;
fn parse(_input: &str) -> IResult<&str, Vec<Report>> {
separated_list1(line_ending, separated_list1(space1, complete::i32))(_input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process() -> miette::Result<()> {
let input = "7 6 4 2 1
1 2 7 8 9
9 7 6 2 1
1 3 2 4 5
8 6 4 4 1
1 3 6 7 9";
assert_eq!("4", process(input)?);
Ok(())
}
}
As you can notice, the two parts are pretty similar. The only difference is the make_safe
function that removes one element from the report and checks if the sequence is safe. This was enough to solve the second part of the problem and get the result I wanted.
Conclusion
I finished the first two days of Advent of Code. I learned a lot about Rust and how to structure a project. Not only that, but I also learned about nom
, itertools
, miette
, and tracing
. The current state is 4 stars out of 20. I hope to complete the remaining days and learn more about Rust.
That’s it for today. I hope you enjoyed this post. See you! Thank you for reading.
Federico