Welcome to my Advent of Code adventure for 2022! This year, I am going to solve the Advent of Code challenges using three different languages - Ruby, Crystal, and Rust. The goal is not to golf for the most terse answer in each language, but rather to implement reasonably idiomatic, but conceptually similar solutions in each language, and to use that as a tool to compare and contrast the languages. So, without further adieu, let's get started!
The Repository
All of the code for this day's solutions, as well as the data input file, can be found at:
https://github.com/wyhaines/advent-of-code-2022/tree/main/day_1
Part 1
To see the full details of the challenge, visit the Advent of Code 2022 - Day 1 page. However, the tl;dr version of the challenge is as follows:
In this puzzle, a group of Elves is going on an expedition to collect star fruit for Santa's reindeer. Each Elf is carrying food items with different numbers of calories. The puzzle is to determine which Elf is carrying the most calories and how many total calories they are carrying. The calories of each food item are listed in a sequence of numbers, with a blank line separating the inventory of each Elf. To solve the puzzle, the numbers must be read and the total calories for each Elf must be calculated. The Elf with the highest total calories is the answer to the puzzle.
So, for example, given the following input:
1000
2000
3000
4000
5000
6000
7000
8000
9000
10000
The fourth elf would be carrying 7000 + 8000 + 9000 == 24000
calories and that would be the answer to this puzzle.
The Approach
The approach to solving this puzzle involves the following steps:
read the input
split it into chunks delimited by repeated newlines
map each chunk into an array of integers
sum the individual arrays of integers
find the largest sum
This is a pretty straightforward approach, and it can be implemented in Ruby, Crystal, and Rust in similar ways.
Ruby solution
The Ruby solution that I wrote looks like this:
the_most_calories = File.read('input.txt')
.split(/\n\n/)
.map do |group|
group
.strip
.split(/\n/)
.map(&:to_i)
end
.map(&:sum)
.max
puts the_most_calories
The solution breaks down into the following steps.
the_most_calories = File.read('input.txt')
.split(/\n\n/)
This solution assumes that the input is in a file called input.txt
.
That file is read into memory and then split into chunks delimited by repeated newlines (i.e. a blank line).
.map do |group|
group
.strip
.split(/\n/)
.map(&:to_i)
end
The resulting array is then mapped, splitting each chunk into an array of text lines, which are then converted, via map(&:to_i)
, into an array of integers. At this point, the program holds an array of arrays of integers.
.map(&:sum)
.max
The next step is to sum each of the inner arrays, and then to find the maximum of those sums. This is done via the map(&:sum).max
method calls.
Crystal solution
For this first task, the Crystal solution is nearly identical to the Ruby solution:
the_most_calories = File.read("input.txt")
.split(/\n\n/)
.map do |group|
group
.strip
.split(/\n/)
.map(&.to_i)
end
.map(&.sum)
.max
puts the_most_calories
The only difference is that the &.
is used instead of the &:
syntax for the map()
inline method calls and double quotes are used around the filename instead of single quotes.
Rust solution
The Rust solution is conceptually identical to the Ruby and Crystal solutions, but the syntax is a bit different, and there is some additional explicit type annotation that is required.
use std::fs;
fn main() {
let the_most_calories: i32 = fs::read_to_string("input.txt")
.unwrap()
.split("\n\n")
.map(|group| {
group
.trim()
.split("\n")
.map(|line| line.parse::<i32>().unwrap())
})
.map(|numbers| numbers.sum())
.max()
.unwrap();
println!("{}", the_most_calories);
}
The most interesting aspect, for me, of solving this with Rust was just how similar the Rust solution ended up being to the Ruby and the Crystal solutions. Rust is more verbose, it requires more ceremony, and its compiler's type inference requires more explicit type annotations than Crystal requires, but the overall structure of the solution still ends up being very similar to the other two, down to the names of the methods that are used in the solution.
Because a Rust program requires a main()
function to execute anything, the program structure can't be quite so simple as the Ruby and Crystal structures, but for this first task, I just put all of the logic directly into main()
.
let the_most_calories: i32 = fs::read_to_string("input.txt")
.unwrap()
.split("\n\n")
As with the previous implementations, the first thing to be done is to read the input file, and to split it into chunks of lines, delimited by a double newline. The unwrap() call is used because fs::read_to_string() returns a Result
, which unwrap()
handles, returning either the data contained in the Result
, or a panic
(an error) of the result is None
.
.map(|group| {
group
.trim()
.split("\n")
.map(|line| line.parse::<i32>().unwrap())
})
The next step, as with the previous examples, is to split each of the chunks of lines into individual lines, and convert them to integers. The syntax is a little more involved, however. The first significant difference is that trim()
call. With Ruby and with Crystal, whitespace is ignored when text is being cast to an integer via to_i()
. With Rust, however, if unexpected whitespace is encountered, you can get an error:
thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: ParseIntError { kind: Empty }', ./day_1_2.rs:11:49
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Using the trim()
method ensures that the input is clean. Following that, it is split just like with the other languages, and then, like the other languages, a map()
call is made. The specific syntax to transform text to an integer is different, but the end result is the same. Once that map()
runs, the array of text chunks has become an array of arrays of integers.
.map(|numbers| numbers.sum())
.max()
.unwrap();
That final map()
sums the array of integers, replacing it with the result of that call, and the max()
method call that follows finds the maximum value in the array.
That's pretty straightforward. There are some small syntax differences, but ultimately the Rust implementation doesn't stray too far from the Ruby and the Crystal versions.
Part 2
The gist of the second part of the puzzle is as follows:
The Elves are interested in finding the sum of the Calories carried by the top three Elves carrying the most Calories to avoid running out of snacks. In the example data given, the top three Elves are the fourth Elf (with 24000 Calories), then the third Elf (with 11000 Calories), then the fifth Elf (with 10000 Calories). The sum of the Calories carried by these three elves is 45000.
The Approach
The approach to solving this puzzle is nearly identical to the approach used to solve part 1. The steps are as follows:
read the input
split it into chunks delimited by repeated newlines
map each chunk into an array of integers
sum the individual arrays of integers
sort the list of sums
take the last three elements of the list
Here is how to make those changes to the Ruby, Crystal, and Rust solutions.
Ruby solution
the_most_calories = File.read('input.txt')
.split(/\n\n/)
.map do |group|
group
.strip
.split(/\n/)
.map(&:to_i)
end
.map(&:sum)
.sort
.last(3)
puts the_most_calories.inspect
puts the_most_calories.sum
The only difference is at the end of the chain of methods. The sort
method is used to sort the list of sums, and then the last(3)
method is used to select the last three elements of the list.
Crystal solution
the_most_calories = File.read("input.txt")
.split(/\n\n/)
.map do |group|
group
.strip
.split(/\n/)
.map(&.to_i)
end
.map(&.sum)
.sort
.last(3)
puts the_most_calories.inspect
puts the_most_calories.sum
Again, the only difference is that the sort
method is used to sort the list of sums, and then the last(3)
method is used to select the last three elements of the list.
Rust solution
use std::fs;
fn main() {
let mut the_most_calories: Vec<i32> = fs::read_to_string("input.txt")
.unwrap()
.split("\n\n")
.map(|group| {
group
.trim()
.split("\n")
.map(|line| line.parse::<i32>().unwrap())
.sum()
})
.collect::<Vec<_>>();
the_most_calories.sort_by(|a, b| b.cmp(a));
the_most_calories.truncate(3);
println!(
"{:?}\n{}",
the_most_calories,
the_most_calories.iter().sum::<i32>()
);
}
The implementation for Rust again follows closely the implementations for Ruby and for Crystal, with one notable difference. In the Crystal and the Ruby versions, it was possible to chain all of the method calls back to back, because Ruby and Crystal both have sort
methods that return the sorted array. With Rust, however, the various sorting methods such as sort()
and sort_by()
act on their argument directly, sorting the elements in place, and returning ()
instead of the array. The truncate()
method does the same. Thus, in the Rust example, we break the call chain and use separate statements to sort and to truncate.
the_most_calories.sort_by(|a, b| b.cmp(a));
the_most_calories.truncate(3);
Conclusion
This was a fun puzzle, and it served as a good introduction to the general ways in which conceptually similar code can vary between the chosen languages, particularly between Rust and the other two languages. The key differences with the Rust version were the need to explicitly annotate the types of variables, and the need to explicitly unwrap the Result
values returned by several of the methods, but the solutions were otherwise quite similar across all three languages.