I Was Told There Would Be No Math
AoC 2015 with Rust - Day 2
Jul 16 23 • 13 min read
Link to problem
Source of the final solution
Other posts in the series
Part 1
Elves are wrapping gift boxes (rectangular prism), and they need some wrapping paper. They want to find out the exact size they need to order. The formula for calculating the size is:
(2*l*w + 2*w*h + 2*h*l) + smallest side's area
For example, a present with dimensions 2x3x4
requires 2*6 + 2*12 + 2*8 = 52
square feet of wrapping paper plus 6
square feet of slack, for a total of 58
square feet.
The input will be a list of such dimensions and the output of the program should be the total square feet of wrapping paper they should order.
It looks like I will have to find a suitable data type that can hold width/length/height values. I can either use some sort of struct or an array. Arrays should be the more suitable data type for this case.
Arrays and Slices page of Rust doc says that arrays require defining their lengths during compilation, which is not really suitable for holding the list of gift sizes since I cannot predict the length of it. So, I am going go with the alternative that can has a dynamic size: slices. The syntax looks something like this:
let empty_array: [u32; 0] = [];
But I can use array for holding the actual sizes of each gifts respectively in arrays with fixed-sizes of three, for width, length, and height. So, arrays within a slice should do it.
let gift_list: [[i32; 3]; 0] = [];
I will also need to read and parse a text file that will hold the list of gifts, with each gift size in one line in 1x1x1
format.
With a quick search, I find fs
module under Rust’s standard library, which has a method called read_to_string
that takes in a file path as its sole parameter, and returns the contents as the string.
Well… not quite. Actually, it returns a Result
enum which has two variants, Ok()
and Err()
. If everything goes accordingly, an Ok()
case should resolve succesfully, giving us the string that I want. I paste the input into a file titled data.txt
and try to print its contents. I initially encountered an error practically saying that data.txt
did not exist, because Rust expected the file to be in the root directory of the project, not at the same level with main.rs
under src
. Moving it to the root did the trick. Anyway, our code now looks like this:
use std::fs;
fn main() {
let gift_list: [[i32; 3]; 0] = [];
let filename = "data.txt";
let contents = fs::read_to_string(filename);
match contents {
Ok(v) => println!("contents of the file: {}", v),
Err(e) => println!("error parsing header: {}", e),
}
}
The Result page on docs also show some other methods for handling error cases like expect()
, which also looks nice (but there are some warnings about its usage, which I can wisely ignore).
use std::fs;
fn main() {
let gift_list: [[i32; 3]; 0] = [];
let filename = "data.txt";
let contents = fs::read_to_string(filename).expect("Something went wrong");
println!("With text:\n{contents}");
}
I now have all the contents of the file as a string in contents
variable. I now have to parse it. I want to split this string into separate lines first, effectively separating each gift size, then split each gift size by looking at the x
. Like many other languages, Rust has a split()
method. I can call this method with a “newline character” but, while looking for possible solutions, I also saw a method called lines()
, which supposedly splits a string into separate lines. I can use this one for the first part. Note that it returns a Lines
iterator, but it’s fine. I want to iterate on this anyway.
fn main() {
let gift_list: [[i32; 3]; 0] = [];
let filename = "data.txt";
let contents = fs::read_to_string(filename).expect("Something went wrong");
let lines = contents.lines();
for line in lines {
// do stuff with line
}
}
I used split()
as mentioned, which also returns an iterator.
let sizes = line.split('x');
I assume that sizes can only have three elements for w/l/h, so it would be handy to directly cast them into an array of size three while also casting strings into integers so I can do math. Again, I find lots of options, the simplest being a good old for
loop.
for line in lines {
let sizes = line.split('x');
for size in sizes {
println!("{}", size);
}
}
But I want to slowly leave the intuitive ways aside and do it the Rust way.
let sizes = line.split('x').collect::<Vec<&str>>();
This lets me convert the Split
type to a good old vector. Combining multiple searches about how to map over iterators and how to cast strings to integers, I came up with a solution like this:
let sizes = line
.split('x')
.map(|size| size.parse::<i32>().unwrap())
.collect::<Vec<i32>>();
At this point, I realize that it is not as easy as I thought to push elements into slices in Rust so while trying to circumvent the issue. Then I realize, I now have line count of the input. So, instead of a slice, I can maybe declare an array with a fixed-size of line count?
Aand, Rust did not like that too. Rust wants to see the array size at compile time, but I am trying to assign it at runtime by looking at the line count. I checked if Vec
s are any different than slices or arrays, and the official docs say that they are resizable, so I try using a vector instead.
use std::fs;
fn main() {
let filename = "data.txt";
let contents = fs::read_to_string(filename).expect("Something went wrong");
let lines = contents.lines();
let line_count = lines.clone().count();
let mut gift_list = vec![vec![0; 3]; line_count];
for line in lines {
let sizes = line
.split('x')
.map(|size| size.parse::<i32>().unwrap())
.collect::<Vec<i32>>();
gift_list.push(sizes);
}
for gift in gift_list {
for size in gift {
println!("{}", size);
}
}
}
Also note that I cloned lines
before checking its size, otherwise it gets consumed and can no longer be used. Something’s going on here (ownership?), but I will look into this later.
Cool, I can iterate over the gift_list
that I have put together. At this point, I can maybe call a function that takes in a vector. I devised something like this but all kinds of mutability rules threw all kinds of errors.
use std::fs;
fn main() {
let filename = "data.txt";
let contents = fs::read_to_string(filename).expect("Something went wrong");
let lines = contents.lines();
let line_count = lines.clone().count();
let mut gift_list = vec![vec![0; 3]; line_count];
for line in lines {
let sizes = line
.split('x')
.map(|size| size.parse::<i32>().unwrap())
.collect::<Vec<i32>>();
gift_list.push(sizes);
}
let total_area: i32 = gift_list.into_iter().map(|v| calculate_area(v)).sum();
println!("{}", total_area);
}
// (2*l*w + 2*w*h + 2*h*l) + smallest side's area
fn calculate_area(v: Vec<i32>) -> i32 {
v.sort();
let smallest_side = v[0] * v[1]; // since vector is now sorted, we can use first two elements for smallest side
(2 * v[0] * v[1]) + (2 * v[0] * v[2]) + (2 * v[1] * v[2]) + smallest_side
}
Rust wants extra annotations for values that I want to change (mutate). I add this to function’s parameter definitions.
Finally, our program seems ready to solve the first part of the puzzle. And, nice. First try! First star.
Part 2
Elves now need ribbons to wrap the gifts. They need some to tie it and some for making a bow. The required length for tying it is the length of its smallest perimeter. And the bow is equal to its cubic feet.
For example, a present with dimensions 2x3x4 requires 2+2+3+3 = 10 feet of ribbon to wrap the present plus 234 = 24 feet of ribbon for the bow, for a total of 34 feet.
It seems I will have to work on sorted vectors again so I move the sorting part to where I initially populate the vector, and fix mut annotation accordingly:
use std::fs;
fn main() {
let filename = "data.txt";
let contents = fs::read_to_string(filename).expect("Something went wrong");
let lines = contents.lines();
let line_count = lines.clone().count();
let mut gift_list = vec![vec![0; 3]; line_count];
for line in lines {
let mut sizes = line
.split('x')
.map(|size| size.parse::<i32>().unwrap())
.collect::<Vec<i32>>();
sizes.sort();
gift_list.push(sizes);
}
let total_area: i32 = gift_list.into_iter().map(|v| calculate_area(v)).sum();
println!("{}", total_area);
}
// (2*l*w + 2*w*h + 2*h*l) + smallest side's area
fn calculate_area(v: Vec<i32>) -> i32 {
let smallest_side = v[0] * v[1]; // since vector is now sorted, we can use first two elements for smallest side
(2 * v[0] * v[1]) + (2 * v[0] * v[2]) + (2 * v[1] * v[2]) + smallest_side
}
I will add two additional functions for ribbons: one for tying and one for the bow, and an additional function that adds the two.
fn get_ribbon_size(v: Vec<i32>) -> i32 {
get_smallest_perimeter(v) + get_volume(v)
}
fn get_smallest_perimeter(v: Vec<i32>) -> i32 {
(v[0] * 2) + (v[1] * 2)
}
fn get_volume(v: Vec<i32>) -> i32 {
v[0] * v[1] * v[2]
}
get_ribbon_size
tells me something about the second function calls argument, something with moved values… the time has come.
Obligatory sidebar: Borrow Checking
Rust does not utilize a garbage collector to free up memory that is not needed anymore, but they say it is as memory-safe as it gets. Rust, by design, wants to prevent us moving data around freely to better decide when to free up memory. It essentialy gives us the convenience of garbage-collection without making us manually manage the memory. All thanks to Rust’s unique ownership model.
When passing data around, I can:
- Directly pass the value, giving up the ownership in process
- Clone the value and work on the clone instead
- Pass a reference, letting the borrower use it until it’s done
Putting it all together, I get something like this:
use std::fs;
fn main() {
let filename = "data.txt";
let contents = fs::read_to_string(filename).expect("Something went wrong");
let lines = contents.lines();
let line_count = lines.clone().count();
let mut gift_list = vec![vec![0; 3]; line_count];
for line in lines {
let mut sizes = line
.split('x')
.map(|size| size.parse::<i32>().unwrap())
.collect::<Vec<i32>>();
sizes.sort();
gift_list.push(sizes);
}
let total_area: i32 = gift_list
.clone()
.into_iter()
.map(|v| calculate_area(v))
.sum();
let ribbon_length: i32 = gift_list
.clone()
.into_iter()
.map(|v| get_ribbon_size(&v))
.sum();
println!(
"Wrapping paper needed: {}\nRibbon needed: {}",
total_area, ribbon_length
);
}
// (2*l*w + 2*w*h + 2*h*l) + smallest side's area
fn calculate_area(v: Vec<i32>) -> i32 {
let smallest_side = v[0] * v[1]; // since vector is now sorted, we can use first two elements for smallest side
(2 * v[0] * v[1]) + (2 * v[0] * v[2]) + (2 * v[1] * v[2]) + smallest_side
}
fn get_ribbon_size(v: &Vec<i32>) -> i32 {
get_smallest_perimeter(v) + get_volume(v)
}
fn get_smallest_perimeter(v: &Vec<i32>) -> i32 {
(v[0] * 2) + (v[1] * 2)
}
fn get_volume(v: &Vec<i32>) -> i32 {
v[0] * v[1] * v[2]
}
Again, this works as expected. Nice.