RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)
First day of week #2, let’s take a look at day 8
You can find the final code @ advent-of-code/2023/day8
We are given the following input
RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)
Starting with AAA, you need to look up the next element based on the next left/right instruction in your input.
If you run out of left/right instructions, repeat the whole sequence of instructions as necessary
The input could be seen as a graph that we have to navigate, starting from
AAA
, following the first line instructions until we end up at ZZZ
.
I would like to implement some kind of state machine that keeps track of the
current position and the graph model in a HashMap
.
#[derive(Debug)]
struct MapModel {
pos: String,
map: HashMap<String, (String, String)>,
}
To parse the input we could make use of a regex, let’s add the crate to our
program with cargo add regex once_cell
. We can now define our regex and
parsing logic of our MapModel
type.
static RE: Lazy<Regex> =
Lazy::new(|| Regex::new(r"([A-Z]{3}) = \(([A-Z]{3}), ([A-Z]{3})\)").unwrap());
impl From<&str> for MapModel {
fn from(value: &str) -> Self {
let mut map = HashMap::new();
for line in value.lines() {
let captures = match RE.captures(&line) {
None => panic!("cannot parse line"),
Some(captures) => captures,
};
let key = captures[1].to_string();
let left = captures[2].to_string();
let right = captures[3].to_string();
map.insert(key, (left, right));
}
MapModel {
pos: String::from("AAA"),
map,
}
}
}
We have the base layer of the implementation, let’s now define two methods that
are going to change the pos
of the state machine by taking the left or right
path of the graph at the current position.
impl MapModel {
fn left(&mut self) {
let (l, _) = self.map.get(&self.pos).unwrap();
self.pos = l.to_owned();
}
fn right(&mut self) {
let (_, r) = self.map.get(&self.pos).unwrap();
self.pos = r.to_owned();
}
}
We now just have to iterate over the first line instructions until we reach ZZZ
, here’s part 1 solution
fn part1(input: &str) -> io::Result<u32> {
let (track, map) = input.split_once("\n\n").unwrap();
let track: Vec<char> = track.chars().collect();
let mut map = MapModel::from(map);
let mut i = 0;
loop {
match track[i % track.len()] {
'R' => map.right(),
'L' => map.left(),
_ => panic!("invalid move"),
};
if map.pos == "ZZZ" {
return Ok(i as u32 + 1);
}
i += 1;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(
2,
part1(
r"RL
AAA = (BBB, CCC)
BBB = (DDD, EEE)
CCC = (ZZZ, GGG)
DDD = (DDD, DDD)
EEE = (EEE, EEE)
GGG = (GGG, GGG)
ZZZ = (ZZZ, ZZZ)",
)
.unwrap()
)
}
}
And that’s it for part 1, pretty straightforward. Let’s jump on part 2!
Part 2 states that we now should start simultaneously to every node that ends
with A
and we should advance every node following the first line instructions,
the program should stop when every node that we end up on ends with Z
.
There’s little logic to change from part 1, we can reuse most of it. To find the
solution to this problem it is obvious that every node has to cycle back at some
point after reaching the nodes ending with Z
(you can test this and you’ll
notice if it’s not), we therefore have to find the length of each cycle
and at that point we know that each node is going to end up on nodes ending with
Z
at the least common multiple of all those values that we’ve found.
Let’s run 'cargo add lcmx' so that we don’t have to code the least common
multiple function. With that, we can calculate the length of the cycle by
calculating the total steps moving from the intial node ending with A
to the
final node ending with Z
.
fn part2(input: &str) -> io::Result<u64> {
let (track, map) = input.split_once("\n\n").unwrap();
let track: Vec<char> = track.chars().collect();
let mut map = MapModel::from(map);
let mut starting_pos: Vec<String> = Vec::new();
for k in map.map.keys().filter(|x| x.ends_with("A")) {
starting_pos.push(k.to_owned());
}
let mut steps: Vec<u64> = Vec::new();
for sp in starting_pos {
map.pos = sp;
let mut i = 0;
loop {
match track[i % track.len()] {
'R' => map.right(),
'L' => map.left(),
_ => panic!("invalid move"),
};
if map.pos.ends_with("Z") {
steps.push(i as u64 + 1);
break;
}
i += 1;
}
}
Ok(lcmx(&steps).unwrap())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part2() {
assert_eq!(
6,
part2(
r"LR
11A = (11B, XXX)
11B = (XXX, 11Z)
11Z = (11B, XXX)
22A = (22B, XXX)
22B = (22C, 22C)
22C = (22Z, 22Z)
22Z = (22B, 22B)
XXX = (XXX, XXX)",
)
.unwrap()
)
}
}
And that gives us the correct answer. I’ve spend almost half an hour trying to
understand why the answer to the problem was wrong and always too low, just to
find out that lcmx
overflowed u32
type, so make sure to use u64
in this
case!
You can find the final code @ advent-of-code/2023/day7
Day 7 was pretty cool, not hard, but a bit verbose.
We have the following input
32T3K 765
T55J5 684
KK677 28
KTJJT 220
QQQJA 483
You get a list of hands, and your goal is to order them based on the strength of each hand. A hand consists of five cards labeled one of A, K, Q, J, T, 9, 8, 7, 6, 5, 4, 3, or 2. The relative strength of each card follows this order, where A is the highest and 2 is the lowest.
Every hand is exactly one type: Five of a kind, Four of a kind, Full house, Two pairs, One pair, High card
What we have to do in this case is reorder the cards that are listed in the input with the logic explained above, once the deck is reordered we have to multiply its index with the hand bid.
Let’s start with a struct that encapsulates a hand
#[derive(Debug)]
struct Hand {
typ: HandType,
cards: Vec<Card>,
bid: u32,
}
#[derive(Debug)]
enum HandType {
HighCard,
OnePair,
TwoPair,
ThreeOfAKind,
FullHouse,
FourOfAKind,
FiveOfAKind,
}
impl From<&Vec<char>> for HandType {
fn from(value: &Vec<char>) -> Self {
let mut occurrencies: HashMap<char, u32> = HashMap::new();
for card in value {
match occurrencies.get_mut(&card) {
Some(v) => {
*v += 1;
}
None => {
occurrencies.insert(*card, 1);
}
}
}
let values: Vec<u32> = occurrencies.into_values().collect();
let r#type = match values.len() {
1 => HandType::FiveOfAKind,
4 => HandType::OnePair,
5 => HandType::HighCard,
3 => match values.contains(&3) {
true => HandType::ThreeOfAKind,
false => HandType::TwoPair,
},
2 => match values.contains(&4) {
true => HandType::FourOfAKind,
false => HandType::FullHouse,
},
_ => panic!("hand with too many cards"),
};
r#type
}
}
impl From<&str> for Hand {
fn from(value: &str) -> Self {
let (cards, bid) = value.split_once(" ").unwrap();
let cards = cards.chars().collect();
let typ = HandType::from(&cards);
let cards = cards.iter().map(Card::from).collect();
Self {
cards,
bid: bid.parse().unwrap(),
typ,
}
}
}
#[derive(Debug)]
enum Card {
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
T,
Jack,
Qeen,
King,
Ace,
}
impl From<&char> for Card {
fn from(value: &char) -> Self {
match value {
'A' => Card::Ace,
'Q' => Card::Qeen,
'K' => Card::King,
'J' => Card::Jack,
'T' => Card::T,
'9' => Card::N9,
'8' => Card::N8,
'7' => Card::N7,
'6' => Card::N6,
'5' => Card::N5,
'4' => Card::N4,
'3' => Card::N3,
'2' => Card::N2,
_ => panic!("invalid card"),
}
}
}
Each time we parse our Hand
from an input line, we immediately get its type,
we should have all the basic logic setup and we can proceed with the
reordering, but first: TESTS.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_handtype_from() {
assert_eq!(
HandType::FiveOfAKind,
HandType::from(&vec!['A', 'A', 'A', 'A', 'A'])
);
assert_eq!(
HandType::FourOfAKind,
HandType::from(&vec!['A', 'Q', 'Q', 'Q', 'Q'])
);
assert_eq!(
HandType::FullHouse,
HandType::from(&vec!['A', 'A', 'J', 'J', 'J'])
);
assert_eq!(
HandType::TwoPair,
HandType::from(&vec!['A', 'A', 'K', 'K', 'Q'])
);
assert_eq!(
HandType::OnePair,
HandType::from(&vec!['A', 'T', 'A', 'K', 'Q'])
);
assert_eq!(
HandType::HighCard,
HandType::from(&vec!['A', 'T', 'Q', 'J', 'K'])
);
}
#[test]
fn test_hand_from() {
assert_eq!(
Hand::new(vec!['A', 'Q', 'J', 'K', 'T'], 10, HandType::HighCard),
Hand::from("AQJKT 10")
);
assert_eq!(
Hand::new(vec!['A', 'A', 'K', 'K', 'T'], 35, HandType::TwoPair),
Hand::from("AAKKT 35")
);
}
}
Everything seems to be fine, let’s move on with the ordering logic. You may argue that all those enums are useless, or that you could have solved this problem without them. I’ve used enums for a cool feature that Rust gives us for free in this case: ordering!
Turns out that we can #[derive]
ordering without writing a single line of
code, but how exactly does Rust decide which value comes first and which value
comes next? That is on us! Values are ordered exactly the way they are defined.
If you take a look at the Card
enum type, Rust will give automatically derive
that N2 < N3 < N4
and so on, how cool is that? Let’s derive more stuff in our enums.
// ordering is also derived for this struct, it will sort first by
// `typ` and then by `cards` and then by `bid`
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
struct Hand {
typ: HandType,
cards: Vec<Card>,
bid: u32,
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum HandType {
...
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Card {
...
}
Quick proof
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_card_ord() {
let mut rnd = vec![
Card::Ace,
Card::Qeen,
Card::King,
Card::Jack,
Card::T,
Card::N6,
Card::N9,
];
rnd.sort();
assert_eq!(
vec![
Card::N6,
Card::N9,
Card::T,
Card::Jack,
Card::Qeen,
Card::King,
Card::Ace,
],
rnd
);
}
}
I just discovered this to be honest, that’s super cool, imagine writing all the ordering logic for all those types and cards?! No thanks.
Let’s sketch part 1 solution, which is trivial at this point
fn part1(input: &str) -> io::Result<u32> {
let mut hands: Vec<Hand> = input.lines().map(Hand::from).collect();
hands.sort();
let sum = hands
.into_iter()
.enumerate()
.map(|(i, card)| card.bid * (i as u32 + 1))
.sum();
Ok(sum)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(
6440,
part1("32T3K 765\nT55J5 684\nKK677 28\nKTJJT 220\nQQQJA 483").unwrap()
)
}
}
Success! Curious about part 2?
Part 2 tells us that we can now use the Jack
card as a jolly and change it to
whichever card is needed to make the hand the strogest possible. When we need to
order by cards though we should consider J
as the weakest card, so J < N2 <
N3
etc.
Since we relied on #[derive]
for ordering, I’ll need to edit the position of
J
in Card
and break part1
solution. I’ll also need to edit the logic with
which I calculate which hand typ
we have.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Card {
Jack,
N2,
N3,
N4,
N5,
N6,
N7,
N8,
N9,
T,
Qeen,
King,
Ace,
}
impl From<&Vec<char>> for HandType {
fn from(value: &Vec<char>) -> Self {
let mut occurrencies: HashMap<char, u32> = HashMap::new();
for card in value {
match occurrencies.get_mut(&card) {
Some(v) => {
*v += 1;
}
None => {
occurrencies.insert(*card, 1);
}
}
}
// remove all ocurrencies of J
if let Some(js) = occurrencies.remove(&'J') {
if js == 5 {
occurrencies.insert('A', 5);
} else {
// Find the key with the max value, that is going the one to
// increase so that we'll have a more powerful hand
let (max_key, max_value) =
occurrencies.iter().max_by_key(|&(_, value)| value).unwrap();
occurrencies.insert(*max_key, max_value + js);
}
}
// same as before
let values: Vec<u32> = occurrencies.into_values().collect();
let r#type = match values.len() {
1 => HandType::FiveOfAKind,
4 => HandType::OnePair,
5 => HandType::HighCard,
3 => match values.contains(&3) {
true => HandType::ThreeOfAKind,
false => HandType::TwoPair,
},
2 => match values.contains(&4) {
true => HandType::FourOfAKind,
false => HandType::FullHouse,
},
_ => panic!("hand with too many cards"),
};
r#type
}
}
That’s all we need to do to get the correct answer for part 2, which is indeed identical to fn part1()
.
fn part2(input: &str) -> io::Result<u32> {
let mut hands: Vec<Hand> = input.lines().map(Hand::from).collect();
hands.sort();
let sum = hands
.into_iter()
.enumerate()
.map(|(i, card)| card.bid * (i as u32 + 1))
.sum();
Ok(sum)
}
There we go, we have the correct solution for the last part of day 7. The
#[derive(PartialOrd, Ord)]
was quite nice to learn, still, today’s problem was
quite verbose! As we move on I might skip some solutions here as they take just
too much space, but you’ll find all the solution on my GitHub :)
I am happy to tell that Day 6 was indeed easier than Day 5, let’s make this quick.
You can find the final code @ advent-of-code/2023/day6
We have the following input
Time: 7 15 30
Distance: 9 40 200
The first line lists the total time that is allowed for a race and the second line contains the record distance that has been recorded for that race.
Your toy boat has a starting speed of zero millimeters per millisecond. For each whole millisecond you spend at the beginning of the race holding down the button, the boat’s speed increases by one millimeter per millisecond.
We can easily solve this with a bit of calculus and math! Let’s take the second race for example, there has been a max recorded distance of 40mm and the allowed time for that race is 15ms.
We can model this problem with a second grade inequality function of this type:
x * (15 - x) > 40
which equates to
x^2 -15x + 40 < 0
This inequality indicates all the different values possible to end up with a
greater distance of 40mm for that race, in our case we only have to consider
discrete values. If we draw that function we’ll see a parabola facing upward and
intersecting y = 0
in two different points, those are the min and max values
of all the different values that will take us further than 40mm.
Take a look at the function in WolframAlpha
As usual, I’d like to create a struct to represent an equation of type ax^2 + bx + c
:
struct Eq {
a: f64,
b: f64,
c: f64,
}
We have our equation, we need a function that calculates the intersection points
with y = 0
and return that as our range of possible values.
impl Eq {
fn new(a: f64, b: f64, c: f64) -> Self {
Self { a, b, c }
}
fn range(&self) -> (i32, i32) {
let discriminant = self.b * self.b - 4.0 * self.a * self.c;
if discriminant >= 0.0 {
let mut root1 = (-self.b + discriminant.sqrt()) / (2.0 * self.a);
let mut root2 = (-self.b - discriminant.sqrt()) / (2.0 * self.a);
// need to be strictly greater, so we have to remove this
// value in case root1 coincides with a discrete value
if root1.fract() == 0.0 {
root1 -= 1.0;
}
// same here
if root2.fract() == 0.0 {
root2 += 1.0;
}
return (root2.ceil() as i32, root1.floor() as i32);
}
(0, 0)
}
}
We have everything we need at this point, we just need to parse our input, calculate every range of possible values for each race and multiply all the final values together to get our result.
fn part1(input: &str) -> io::Result<u32> {
let lines: Vec<&str> = input.lines().collect::<Vec<&str>>();
let ms: Vec<f64> = lines[0]
.split(" ")
.skip(1)
.filter(|x| !x.is_empty())
.flat_map(|x| x.parse())
.collect();
let records: Vec<f64> = lines[1]
.split(" ")
.skip(1)
.filter(|x| !x.is_empty())
.flat_map(|x| x.parse())
.collect();
let mut eqs = Vec::new();
for i in 0..ms.len() {
eqs.push(Eq::new(1.0, -ms[i], records[i]));
}
let res = eqs
.into_iter()
.map(|x| x.range())
.map(|(lower, upper)| upper - lower + 1)
.reduce(|acc, e| acc * e)
.unwrap();
Ok(res as u32)
}
Quick test
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(
(4 * 8 * 9),
part1("Time: 7 15 30\nDistance: 9 40 200").unwrap()
)
}
}
Solution is correct, nice and easy, let’s move on.
There’s really only one race - ignore the spaces between the numbers on each line.
Lucky me, we don’t have to change any logic but the parting one in this case. Our input now has to be considered as a single time number and a single distance number, that’s it!
fn part2(input: &str) -> io::Result<u32> {
let lines: Vec<&str> = input.lines().collect::<Vec<&str>>();
let ms: f64 = lines[0]
.split(" ")
.skip(1)
.filter(|x| !x.is_empty())
.collect::<String>()
.parse()
.unwrap();
let record: f64 = lines[1]
.split(" ")
.skip(1)
.filter(|x| !x.is_empty())
.collect::<String>()
.parse()
.unwrap();
let eq = Eq::new(1.0, -ms, record);
let (lower, upper) = eq.range();
Ok(upper as u32 - lower as u32 + 1)
}
Testing testing testing
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part2() {
assert_eq!(
71503,
part2("Time: 7 15 30\nDistance: 9 40 200").unwrap()
)
}
}
Easy done, calculus makes everything fun, right?
We’re on a rollercoaster, one day we can get away with an easy solution and the next it’s a lot more complext than that. Day 4 was pretty easy, so Day 5 won’t be :) Let’s take a look at the problem.
You can find the final code @ advent-of-code/2023/day5
We’re given this input sample
seeds: 79 14 55 13
seed-to-soil map:
50 98 2
52 50 48
soil-to-fertilizer map:
0 15 37
37 52 2
39 0 15
fertilizer-to-water map:
49 53 8
0 11 42
42 0 7
57 7 4
water-to-light map:
88 18 7
18 25 70
light-to-temperature map:
45 77 23
81 45 19
68 64 13
temperature-to-humidity map:
0 69 1
1 0 69
humidity-to-location map:
60 56 37
56 93 4
The initial row has 4 different seed values, each seed value has to be mapped to
the corresponding value in each block. Each block (i.e seed-to-soil
) has
different rows that contain the ranges to which each initial value has to be
mapped to, the first block has two different mappings: source range (98, 100),
destination range (50, 52), left range is always excluded.
Let’s take seed 79
, if we map it with seed-to-soil
we get 81
, this is
because 79 lies in the range represented by the line 52 50 48
which is has
source range (50, 50 + 48) = (50, 98)
and maps those values to (52, 52 + 48) = (52, 100)
.
We have to get the location of each seed, which we can get by performing the mappings of each block from top to bottom.
As in many other cases, I would start off by creating a struct which contains mapping values that we later could use.
#[derive(Debug, PartialEq, Eq)]
struct Map {
src: u64,
dst: u64,
rng: u64,
}
impl Map {
fn new(src: u64, dst: u64, rng: u64) -> Self {
Map { src, dst, rng }
}
}
impl From<&str> for Map {
fn from(value: &str) -> Self {
let vals: Vec<&str> = value.split(' ').collect();
let dst = vals[0].parse().unwrap();
let src = vals[1].parse().unwrap();
let rng = vals[2].parse().unwrap();
Map { src, dst, rng }
}
}
I can now represent each map in the input file with a Vec<Map>
, I would like
to implement the mapping logic on that data structure, let’s define a local
trait for that.
trait IntoRangeMapping {
fn get_mapped_value(&self, v: &u64) -> u64;
}
// iterates over all the map ranges
// and returns the corresponding mapped value
impl IntoRangeMapping for Vec<Map> {
fn get_mapped_value(&self, v: &u64) -> u64 {
for map in self {
if (map.src..map.src + map.rng).contains(v) {
return map.dst + (v - map.src);
}
}
*v
}
}
This is all we need to get past part 1
fn part1(input: &str) -> io::Result<u64> {
let parts: Vec<&str> = input.split("\n\n").collect();
let seeds: Vec<u64> = parts[0]
.split(' ')
.skip(1)
.filter_map(|x| x.parse().ok())
.collect();
let mut mappings: Vec<Vec<Map>> = Vec::new();
for i in 1..parts.len() {
let maps: Vec<Map> = parts[i]
.split('\n')
.skip(1)
.filter(|x| !x.is_empty())
.map(Map::from)
.collect();
mappings.push(maps);
}
let mut locations: Vec<u64> = Vec::with_capacity(seeds.len());
for seed in seeds {
let mut value_map = seed;
for i in 0..mappings.len() {
value_map = mappings[i].get_mapped_value(&value_map);
}
locations.push(value_map);
}
Ok(locations.into_iter().min().unwrap())
}
As usual, a quick test
#[cfg(test)]
mod tests {
use super::*;
const INPUT: &str = r"seeds: 79 14 55 13
...
humidity-to-location map:
60 56 37
56 93 4";
#[test]
fn test_map_parse() {
assert_eq!(Map::new(20, 30, 40), Map::from("30 20 40"));
}
#[test]
fn test_range_mapping() {
let rng_map = vec![Map::new(0, 5, 2), Map::new(6, 9, 3)];
assert_eq!(5, rng_map.get_mapped_value(&0));
assert_eq!(6, rng_map.get_mapped_value(&1));
assert_eq!(3, rng_map.get_mapped_value(&3));
assert_eq!(4, rng_map.get_mapped_value(&4));
assert_eq!(5, rng_map.get_mapped_value(&5));
assert_eq!(9, rng_map.get_mapped_value(&6));
assert_eq!(10, rng_map.get_mapped_value(&7));
assert_eq!(11, rng_map.get_mapped_value(&8));
assert_eq!(9, rng_map.get_mapped_value(&9));
assert_eq!(10, rng_map.get_mapped_value(&10));
}
#[test]
fn test_part1() {
assert_eq!(35, part1(INPUT).unwrap());
}
}
Solution is correct! Let’s move on to the troubling part 2.
We now are told that the first seed line does not represent single seeds, but a range of seeds. Also, we now have to return the min value of all the new locations found.
We could brute force the solution by keeping the same logic as above and find the new location for each seed in the range, but it’s going to take a lot of time.
What we could do instead is do some operations on ranges, particularly we could find where ranges end up in the final location, once we have all the location ranges we have to take the min one.
Since we have to find overlapping ranges, let’s add a method that does just that to the Map
type.
impl Map {
fn overlaps_with(&self, r_start: u64, r_end: u64) -> Option<(u64, u64)> {
let left_overlap = cmp::max(r_start, self.src);
let right_overlap = cmp::min(r_end, self.src + self.rng);
match left_overlap < right_overlap {
true => Some((left_overlap, right_overlap)),
false => None,
}
}
}
I want to work with Vec<Map>
here too, so let’s add another method to our trait that will return the new mapped ranges given an initial range.
trait IntoRangeMapping {
fn get_mapped_value(&self, v: &u64) -> u64;
fn get_overlapping_ranges(
&self,
start_range: u64,
end_range: u64,
) -> (Option<(u64, u64)>, Option<(u64, u64)>, Option<(u64, u64)>);
}
impl IntoRangeMapping for Vec<Map> {
fn get_overlapping_ranges(
&self,
start_range: u64,
end_range: u64,
) -> (Option<(u64, u64)>, Option<(u64, u64)>, Option<(u64, u64)>) {
let mut overlapping = None;
let mut left_range = None;
let mut right_range = None;
for map in self {
if let Some((ol, or)) = map.overlaps_with(start_range, end_range) {
overlapping = Some((ol - map.src + map.dst, or - map.src + map.dst));
if ol > start_range {
left_range = Some((start_range, ol));
}
if or < end_range {
right_range = Some((or, end_range));
}
return (overlapping, left_range, right_range);
}
}
(overlapping, left_range, right_range)
}
}
get_overlapping_ranges
will return overlapping
which is the new mapped
range, if present, left_range and right_range in case the range map is contained
by the original range.
Let’s make use of these newly created methods in the final solution
fn part2(input: &str) -> io::Result<u64> {
let parts: Vec<&str> = input.split("\n\n").collect();
// tuple indicating start and end values of range
let mut seed_ranges: Vec<(u64, u64)> = parts[0]
.split(' ')
.skip(1)
.filter_map(|x| x.parse().ok())
.collect::<Vec<u64>>()
.chunks(2)
.map(|w| (w[0], w[0] + w[1]))
.collect();
// same as before
let mut mappings: Vec<Vec<Map>> = Vec::new();
for i in 1..parts.len() {
let mapping: Vec<Map> = parts[i]
.split('\n')
.skip(1)
.filter(|x| !x.is_empty())
.map(Map::from)
.collect();
mappings.push(mapping);
}
// repeat step for each map until we end up
// with final seed range locations
for range_map in mappings {
// keep track of new mapped ranges
let mut next_ranges: Vec<(u64, u64)> = Vec::new();
while let Some((start, end)) = seed_ranges.pop() {
match range_map.get_overlapping_ranges(start, end) {
(None, _, _) => {
// keep same mapping if there is no overlap
next_ranges.push((start, end));
}
(Some(overlapping), lr, rr) => {
next_ranges.push(overlapping);
// these need to be checked in case there is another
// overlap with other ranges maps
if let Some(lr) = lr {
seed_ranges.push(lr);
}
if let Some(rr) = rr {
seed_ranges.push(rr);
}
}
}
}
seed_ranges = next_ranges.clone();
}
Ok(seed_ranges.into_iter().map(|x| x.0).min().unwrap())
}
Let’s test this
#[cfg(test)]
mod tests {
#[test]
fn test_part2() {
assert_eq!(46, part2(INPUT).unwrap());
}
}
Phew, I have to admit that this took me quite a bit! Part 2 solution is correct, let’s hope for an easier Day 6 :)
You can find the final code @ advent-of-code/2023/day4
In Day 4 we have the following input
Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11
In the above example, card 1 has five winning numbers (41, 48, 83, 86, and 17) and eight numbers you have (83, 86, 6, 31, 17, 9, 48, and 53). Of the numbers you have, four of them (48, 83, 17, and 86) are winning numbers! That means card 1 is worth 8 points (1 for the first match, then doubled three times for each of the three matches after the first).
The parsing logic is very similar to day 2, let’s create a struct that contains
the winning numbers and the contained numbers of a card. This time I’m using a
HashSet
since it’s faster to check if a value is contained into another set.
#[derive(Debug)]
struct Card {
winning: HashSet<u32>,
numbers: HashSet<u32>,
}
impl From<&str> for Card {
fn from(value: &str) -> Self {
let (_, all_nums) = value.split_once(":").unwrap();
let (winning_str, numbers_str) = all_nums.split_once("|").unwrap();
let winning: HashSet<u32> = winning_str
.trim()
.replace(" ", " ")
.split(' ')
.map(|x| x.parse().unwrap())
.collect();
let numbers: HashSet<u32> = numbers_str
.trim()
.replace(" ", " ")
.split(' ')
.map(|x| x.parse().unwrap())
.collect();
Card { winning, numbers }
}
}
I want to create a function that counts the matching numbers for a certain card
impl Card {
fn matching(&self) -> u32 {
let mut matching = 0;
for w in &self.winning {
if self.numbers.contains(w) {
matching += 1;
}
}
matching
}
}
After that I need a function to actually calculate the points of each card, which depends on the number of matching winning numbers. The first match gives the card 1 point, after that we have to multiply the card’s points by 2, which is equivalent to pow the card points by 2. Looks like a good case where bit shifting can be used
impl Card {
fn points(&self) -> u32 {
let matching = self.matching();
if matching == 0 {
return 0;
}
1 << (matching - 1)
}
}
That is all we need for part1, let’s run some tests
fn part1(input: &str) -> io::Result<u32> {
let points = input.lines().map(Card::from).map(|x| x.points()).sum();
Ok(points)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(
13,
part1(
"Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11"
)
.unwrap()
);
}
}
Aaand part1 solution is correct! Let’s move on with part 2
The problem states
There’s no such thing as "points". Instead, scratchcards only cause you to win more scratchcards equal to the number of winning numbers you have.
Specifically, you win copies of the scratchcards below the winning card equal to the number of matches. So, if card 10 were to have 5 matching numbers, you would win one copy each of cards 11, 12, 13, 14, and 15.
Copies of scratchcards are scored like normal scratchcards and have the same card number as the card they copied. So, if you win a copy of card 10 and it has 5 matching numbers, it would then win a copy of the same cards that the original card 10 won: cards 11, 12, 13, 14, and 15. This process repeats until none of the copies cause you to win any more cards. (Cards will never make you copy a card past the end of the table.)
We can reuse at least the matching
function of Card
. We now need to calculate
how may cards we end up.
fn part2(input: &str) -> io::Result<u32> {
let matches: Vec<u32> = input
.lines()
.map(Card::from)
.map(|x| x.matching())
.collect();
let mut cards: Vec<u32> = vec![1; matches.len()];
for (i, matching) in matches.iter().enumerate() {
let index = i as u32 + 1;
let incr = cards[i];
// increment the number of cards that are in the window
// (i+1) until ((i+1) + matching) by the number of cards
// at position i
for l in index..index + matching {
if let Some(v) = cards.get_mut(l as usize) {
*v += incr;
}
}
}
Ok(cards.into_iter().sum())
}
I am using a vector that keeps track of how many cards I have in my deck, i.e if cards[10] = 5 I have 5 Card 11, the index mismatch is not important in this case since we need to sum all the number of cards that we have in the deck at the end.
Note that I am using cards.get_mut
because I can only add cards that are
actually present in the deck. So, if the last card in the deck is Card 6 and
Card 6 has 2 matching values I cannot add Card 7 and Card 8, because they’re not
present in the deck, in that case cards.get_mut
will return None
and no
operation is going to be performed.
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part2() {
assert_eq!(
30,
part2(
"Card 1: 41 48 83 86 17 | 83 86 6 31 17 9 48 53
Card 2: 13 32 20 16 61 | 61 30 68 82 17 32 24 19
Card 3: 1 21 53 59 44 | 69 82 63 72 16 21 14 1
Card 4: 41 92 73 84 69 | 59 84 76 51 58 5 54 83
Card 5: 87 83 26 28 32 | 88 30 70 12 93 22 82 36
Card 6: 31 18 13 56 72 | 74 77 10 23 35 67 36 11"
)
.unwrap()
);
}
}
Test passes and the solution is correct!
Day 3 was no walk in the park; it had me revisiting my approach several times.
You can find the final code @ advent-of-code/2023/day3
We have the following input
467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598..
Part 1 of the problem wants us to return the sum of all the numbers that have a
symbol as a neighbor (.
is not a symbol).
The problem is not trivial, I would like to break it down as much as possible so that the solution is easier to read and understand.
At some point we will need to look at the neighbors of a char
, so the first
function that I would implement is one that returns neighboring coordinates of a
coordinate.
type Coord = (usize, usize);
/// Returns neighboring coordinates that are inside of the matrix bounds
fn get_neighbors_coords(n: i32, m: i32, i: i32, j: i32) -> HashSet<Coord> {
let coords = vec![
(-1, -1),
(-1, 0),
(-1, 1),
(0, -1),
(0, 1),
(1, 1),
(1, 0),
(1, -1),
];
let mut nb = HashSet::new();
for (x, y) in coords {
nb.insert((i + x, j + y));
}
nb.into_iter()
.filter(|(x, y)| !(*x < 0 || *y < 0 || *x >= n || *y >= m))
.map(|(x, y)| (x as usize, y as usize))
.collect()
}
I’ll also need a small utility function to check if a char is a valid symbol or not
fn is_symbol(c: &char) -> bool {
c.is_ascii_punctuation() && *c != '.'
}
I would like to also have a function that returns a number given the coordinate
of one of its digits. A number lies on a single row, so we can use a left and
right pointer and move them respectively to the leftmost and rightmost digit
value and parse the slice contained between the two pointers as a u32
.
fn get_num_at_coord(matrix: &Vec<Vec<char>>, coord: &Coord) -> u32 {
let row = coord.0;
let (mut l, mut r) = (coord.1, coord.1);
while l > 0 && matrix[row][l - 1].is_digit(10) {
l -= 1;
}
while r < matrix[row].len() - 1 && matrix[row][r + 1].is_digit(10) {
r += 1;
}
matrix[row][l..=r]
.iter()
.collect::<String>()
.parse()
.unwrap()
}
The last utility function that I would like to implement is the one that is going to actually return the neighboring numbers of a coordinate.
/// Returns all the neighboring numbers given a matrix and a coordinate.
fn neighboring_numbers(matrix: &Vec<Vec<char>>, i: usize, j: usize) -> Vec<u32> {
let mut num = Vec::new();
let (n, m) = (matrix.len() as i32, matrix[0].len() as i32);
for (x, y) in get_neighbors_coords(n, m, i as i32, j as i32) {
if matrix[x][y].is_digit(10) {
num.push((x, y));
}
}
// I'm using a HashSet because I have to consider a single number given
// the coordinates of one of its digits, but num could contain multiple
// coordinates that belong to the same number
//
// Consider for example `..23#111.`
// I would end up with
// num = (0,2), (0,3), (0,5), (0,6), (0,7)
// but `get_num_at_coord((0,2)) == get_num_at_coord((0,3)) == 23` and
// `get_num_at_coord((0,5)) == get_num_at_coord((0,6)) == get_num_at_coord((0,6)) == 111`
let mut pairs: HashSet<u32> = HashSet::new();
for coord in num {
pairs.insert(get_num_at_coord(matrix, &coord));
}
pairs.into_iter().collect()
}
I've cheated a little bit in this case. This function will only return a correct answer if and only if the neighboring numbers all differ from each other. I come from the future and the input that is given to us seems to be okay with this assumption, bear with me.
The core logic of part 1 is now trivial, we have to iterate through all the matrix and find all the neighboring numbers of symbols
fn part1(input: &str) -> io::Result<u32> {
let mut mat: Vec<Vec<char>> = Vec::new();
for line in input.lines() {
mat.push(line.chars().collect());
}
let mut nums = Vec::new();
for i in 0..mat.len() {
for j in 0..mat[0].len() {
if is_symbol(&mat[i][j]) {
nums.extend(neighboring_numbers(&mat, i, j))
}
}
}
Ok(nums.iter().sum())
}
This should be enough to get us through part 1, let’s run some tests to make sure everything is okay
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_neighbors_coors() {
assert_eq!(
HashSet::from_iter(vec![(1, 0), (1, 1), (0, 1)]),
get_neighbors_coords(7, 7, 0, 0)
);
assert_eq!(
HashSet::from_iter(vec![(5, 6), (5, 5), (6, 5)]),
get_neighbors_coords(7, 7, 6, 6)
);
assert_eq!(
HashSet::from_iter(vec![
(2, 2),
(2, 3),
(2, 4),
(4, 2),
(4, 3),
(4, 4),
(3, 2),
(3, 4)
]),
get_neighbors_coords(7, 7, 3, 3)
);
}
#[test]
fn test_part1() {
assert_eq!(
4361,
part1(
r"467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598.."
)
.unwrap()
);
}
}
Bingo! Part 1 passes, let’s move on to Part 2.
Part 2 states that:
A gear is any * symbol that is adjacent to exactly two part numbers. Its gear ratio is the result of multiplying those two numbers together.
We have to return the sum of all the ratios in the input. We can reuse 100% of what we wrote before, I would add a single function to return only valid ratios neighboring numbers.
fn neighboring_number_pair(matrix: &Vec<Vec<char>>, i: usize, j: usize) -> Option<(u32, u32)> {
let pairs = neighboring_numbers(matrix, i, j);
match pairs.len() {
2 => Some((pairs[0], pairs[1])),
_ => None,
}
}
With that, part 2 is very similar to part 1, we just have to iterate through the matrix and sum all the values of the ratios that we find.
fn part2(input: &str) -> io::Result<u32> {
let mut mat: Vec<Vec<char>> = Vec::new();
for line in input.lines() {
mat.push(line.chars().collect());
}
let mut sum = 0;
for i in 0..mat.len() {
for j in 0..mat[0].len() {
if mat[i][j] == '*' {
if let Some((n1, n2)) = neighboring_number_pair(&mat, i, j) {
sum += n1 * n2;
}
}
}
}
Ok(sum)
}
More tests
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part2() {
assert_eq!(
467835,
part2(
"467..114..
...*......
..35..633.
......#...
617*......
.....+.58.
..592.....
......755.
...$.*....
.664.598.."
)
.unwrap()
);
assert_eq!(
(467 * 35) + (617 * 2) + (755 * 598),
part2(
"467..114..
...*......
..35..633.
......#...
617*2.....
.....+.58.
..592.....
......755.
...$.*....
.664.598.."
)
.unwrap()
);
}
}
There we go, part 2 is done! As I noted above, this solution is not 100% correct as I’ve assumed that all the numbers that make up a ratio are different from each other. Indeed, my solution will fail with this input
467..114..
...*......
.467..633.
...$.*....
.664.598..
A quick test will show just that
#[test]
fn test_part2_alternative() {
assert_eq!(
(467 * 467) + (633 * 598),
part2(
"467..114..
...*......
.467..633.
...$.*....
.664.598.."
)
.unwrap()
);
}
running 1 test
test tests::test_part2_alternative ... FAILED
---- tests::test_part2_alternative stdout ----
thread 'tests::test_part2_alternative' panicked at src/main.rs:215:9:
assertion `left == right` failed
left: 596623
right: 378534
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
To solve this you just need to add more conditions to the neighboring_numbers
function, but I’m lazy and I won’t do that today :)
Ready for Day 2? Let’s jump right into it.
You can find the final code @ advent-of-code/2023/day2
We have an input that represents a game, each game has an id and a set of hands. Each hand has the number of cubes grabbed by the bag and its color.
Here’s the input example:
Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green
The first line represents game #1 that has 3 different hands.
We are given the number of cubes that are present in the bag and we have to determine which game is possible with that information. I’m going to quote the problem description to make things clearer
Which games would have been possible if the bag contained only 12 red cubes, 13 green cubes, and 14 blue cubes?
In the example above, games 1, 2, and 5 would have been possible if the bag had been loaded with that configuration. However, game 3 would have been impossible because at one point the Elf showed you 20 red cubes at once; similarly, game 4 would also have been impossible because the Elf showed you 15 blue cubes at once. If you add up the IDs of the games that would have been possible, you get 8.
In this case it could be very useful to create a struct that encapsulates all the game information that we find on each line. Each game has an id and its sets representation. Each set can be represented as a matrix of RGB values.
type RGB = (u32, u32, u32);
#[derive(Debug)]
struct Game {
id: u32,
sets: Vec<RGB>,
}
Now we can implement a From<&str>
trait for the Game
type so that we can use
it later to parse the game information from a single line. The parsing logic is
going to be a bit verbose but I will basically split by :
, ;
and ,
until I
only have RGB values that are going to be stored in the matrix.
impl From<&str> for Game {
fn from(value: &str) -> Self {
let (game_str, sets_str) = value.split_once(":").unwrap();
let id = game_str.split_once(" ").unwrap().1.parse().unwrap();
let mut sets = Vec::new();
for set in sets_str.split(";") {
let mut rgb = (0, 0, 0);
for hand in set.split(",") {
match hand.trim().split_once(" ").unwrap() {
(id, "red") => {
rgb.0 = id.parse().unwrap();
}
(id, "green") => {
rgb.1 = id.parse().unwrap();
}
(id, "blue") => {
rgb.2 = id.parse().unwrap();
}
_ => {}
}
}
sets.push(rgb);
}
Game { id, sets }
}
}
Quick test to validate our parsing implementation
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_game_parse() {
let g = Game::from("Game 123: 2 green, 1 red; 3 red, 4 blue");
assert_eq!(g.id, 123);
assert_eq!(g.sets, vec![(1, 2, 0), (3, 0, 4)]);
}
}
Everything looks good so far, with that let’s move on to the solution of the first part of the problem.
We have to determine if a game is possible given an initial cube configuration. A game is possible if the max number of cubes of a specific color grabbed in a game is less than or equal to the value in the configration.
To make things easier I would like to implement a method that returns the max value of a particular cube color grabbed in a game, so that we can make an immediate comparison when we iterate through all the games in the input.
impl Game {
fn get_max_rgb_value(&self) -> RGB {
let mut max_rgb = (0, 0, 0);
for (r, g, b) in &self.sets {
if max_rgb.0 < *r {
max_rgb.0 = *r;
}
if max_rgb.1 < *g {
max_rgb.1 = *g;
}
if max_rgb.2 < *b {
max_rgb.2 = *b;
}
}
max_rgb
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_max_rgb_value() {
let g1 = Game::from("Game 123: 2 green, 1 red; 3 red, 4 blue");
assert_eq!(g1.get_max_rgb_value(), (3, 2, 4));
let g2 = Game::from("Game 123: 1 red; 3 red, 4 blue");
assert_eq!(g2.get_max_rgb_value(), (3, 0, 4));
let g3 = Game::from("Game 123: 2 green");
assert_eq!(g3.get_max_rgb_value(), (0, 2, 0));
}
}
At this point we just have to iterate through all the games, once they’ve all been parsed, and sum the id of those that satisfy the configuration condition.
fn main() -> io::Result<()> {
let mut input = String::new();
io::stdin().read_to_string(&mut input)?;
writeln!(io::stdout(), "{}", part1(&input)?)?;
Ok(())
}
fn part1(input: &str) -> io::Result<u32> {
let config: RGB = (12, 13, 14);
let sum = input
.lines()
.map(Game::from)
.filter(|x| {
let rgb = x.get_max_rgb_value();
rgb.0 <= config.0 && rgb.1 <= config.1 && rgb.2 <= config.2
})
.map(|x| x.id)
.sum();
Ok(sum)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(0, part1("Game 3: 13 red").unwrap());
assert_eq!(0, part1("Game 3: 14 green").unwrap());
assert_eq!(0, part1("Game 3: 15 blue").unwrap());
assert_eq!(1, part1("Game 1: 10 green; 5 blue").unwrap());
assert_eq!(1, part1("Game 1: 10 green; 5 blue").unwrap());
assert_eq!(1, part1("Game 1: 10 green; 5 blue").unwrap());
assert_eq!(2, part1("Game 2: 12 red").unwrap());
assert_eq!(2, part1("Game 2: 13 green").unwrap());
assert_eq!(2, part1("Game 2: 14 blue").unwrap());
}
}
cat input | cargo run -
returns the correct answer, let’s move to part 2 now.
The problem is now asking to calculate which is the minimum number of cubes and
their colors that could have made the game possible. If you followed along, you
may have noticed that we don’t need to code anymore logic for this. Indeed,
get_max_rgb_value
is all we need to answer that question since that
already returns what the problem is asking. Once we have the minimum number
of cubes that could have made the game possible, we have to multiply those
RGB values and sum all of them to get the final result.
fn part2(input: &str) -> io::Result<u32> {
let sum = input
.lines()
.map(Game::from)
.map(|x| x.get_max_rgb_value())
.map(|(r, g, b)| r * g * b)
.sum();
Ok(sum)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part2() {
assert_eq!(
2286,
part2(
r"Game 1: 3 blue, 4 red; 1 red, 2 green, 6 blue; 2 green
Game 2: 1 blue, 2 green; 3 green, 4 blue, 1 red; 1 green, 1 blue
Game 3: 8 green, 6 blue, 20 red; 5 blue, 4 red, 13 green; 5 green, 1 red
Game 4: 1 green, 3 red, 6 blue; 3 green, 6 red; 3 green, 15 blue, 14 red
Game 5: 6 red, 1 blue, 3 green; 2 blue, 1 red, 2 green"
)
.unwrap()
);
}
}
Again, cat input | cargo run -
returns the correct solution.
I’ve been lucky this time around, day 2 is off the map and we can call it a day, yay!
It’s December and a new Advent of Code is ready to be solved.
This year I’m picking Rust, again. Let’s go ahead and solve Day 1!
You can find the final code @ advent-of-code/2023/day1
Our input is a multi-line text file, each line has variable length and consists of a sequence of chars and digits, here’s the example that is given to us
1abc2
pqr3stu8vwx
a1b2c3d4e5f
treb7uchet
The problem goes on by saying that each line contains a calibration value that we need to get. In order to obtain that value, we have to combine the first digit and the last digit that we find on each line.
Let’s take the second line for example: if we walk every char we encounter 3
as
the first digit and 8
as last digit, therefore, the calibration value is
38
.
What about the last line? Well, we only have a single digit, which is 7
, and
that is both our first and last digit. The calibration value for the last line
is 77
.
This problem is pretty trivial, just what we wanted to warm ourselves for the 24 days ahead.
We have to start by reading the entire input file. It could be a useful exercise
to code an input reader directly in the program but I like to handle this part
by just piping the input into stdin
with cat
.
In Rust we can just create a mutable String
and inject the content of stdin
into it.
use std::io::{self, Read};
fn main() -> io::Result<()> {
let mut input = String::new();
io::stdin().read_to_string(&mut input);
println!("{}", input);
Ok(())
}
We need to import std::io::Read
to use read_to_string
, otherwise the
compiler will complain that that method is not implemented by stdin
.
You can now run this command to see that everything is working as expected:
$ cat input | cargo run -
Now that we have our input, I want to create a function that takes an input and that is returns all the logic for part 1.
fn main() -> io::Result<()> {
let mut input = String::new();
let _ = io::stdin().read_to_string(&mut input);
writeln!(io::stdout(), "{}", part1(&input)?)?;
Ok(())
}
fn part1(input: &str) -> io::Result<u32> {
todo!()
}
Everything is setup, now we need to work on the solution.
What I would do in this case is:
Iterate over each line
Map each line to a the corresponding calibration value
Sum all the values
fn part1(input: &str) -> io::Result<()> {
let sum: u32 = input
.lines()
.map(|line| {
// From the line we get an array of `char` and we try to parse each
// one into a digit with `to_digit`.
let digits: Vec<u32> = line
.chars()
// `to_digit` returns an `Option<u32>` and sice we only need valid
// digits we can get rid of all the `None` values by using
// `flat_map`.
.flat_map(|x| x.to_digit(10))
.collect();
// We parse the calibration value by taking the first and last digit
(digits[0] * 10) + digits[digits.len() - 1]
})
.sum();
writeln!(io::stdout(), "{}", sum)?;
Ok(())
}
To make sure that we did everything correctly I usually do a quick and dirty test
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_part1() {
assert_eq!(part1("ad1f3").unwrap(), 13);
assert_eq!(part1("ad1\na3\n11\n0").unwrap(), 11 + 33 + 11 + 0);
}
}
By running cargo test
everything seems to pass, and if we paste the output
solution returned by cat input | cargo run -
, the AoC portal tells us that the
solution is indeed correct.
We now have access to the second part of the problem: we are told that some of
the digits are actually spelled with letters. For example, 7pqrstsixteen
has a
calibration value of 76
because the first digit is 7 and the last one is
six
.
We could reuse our part1
function if we could somehow replace spelled digits
with its digit char, but a simple replace function is not going to do it in this
case. Take a look at eightwothree
for example, you can have different outcomes
depending on the order of replacement that you apply:
You may end up with 8wo3
if you replace eight
before two
You may end up with eigh23
if you replace two
before eight
The problem description tells us that in this case the correct solution would be
8wo3
, so I guess that we need to replace spelled digits in order of
occurrence.
In this case, we can’t easily reuse the part1
function, but don’t worry, we
can create a brand new function to solve this second part of the problem. I
would solve this second part by doing the following:
For each line find the indices of each digit or spelled digit
Keep track of max digit and min digit
Map line with found digit at min and max position
Sum all the values
Rust has a nice function that returns all the indices of a certain
substring in a string: match_indices
, we can make use of that.
Here’s a quick sketch of the solution
fn repl_digits(x: &str) -> u32 {
let digits = vec![
("1", "1"),
("2", "2"),
("3", "3"),
("4", "4"),
("5", "5"),
("6", "6"),
("7", "7"),
("8", "8"),
("9", "9"),
("0", "0"),
("one", "1"),
("two", "2"),
("three", "3"),
("four", "4"),
("five", "5"),
("six", "6"),
("seven", "7"),
("eight", "8"),
("nine", "9"),
("zero", "0"),
];
// Initialize the first and last occurrences
let mut first: (&str, usize) = ("", x.len());
let mut last: (&str, usize) = ("", 0);
for (substr, digit) in &digits {
// Get the array of indices for the current substring
let occ: Vec<_> = x.match_indices(substr).map(|x| x.0).collect();
// If there's no occurrence, move to the next substr
if occ.len() == 0 {
continue;
}
// The indices array is ordered and we only need the first and last
// occurrences
let (min, max) = (occ[0], occ[occ.len() - 1]);
// If the min index found is lte the current min index
// keep track of the new first digit and its index
if min <= first.1 {
first = (digit, min);
}
// If the max index found is gte the current max index
// keep track of the new last digit and its index
if max >= last.1 {
last = (digit, max);
}
}
// Parse value resulting by concatenating the two
// digits
format!("{}{}", first.0, last.0)
.parse::<u32>()
.unwrap()
}
fn part2(input: &str) -> io::Result<u32> {
let sum: u32 = input.lines().map(repl_digits).sum();
Ok(sum)
}
Quick test to make sure that everything is working properly
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_repl_digits() {
assert_eq!(repl_digits("eightwothree"), 83);
assert_eq!(repl_digits("13eightwothree"), 13);
assert_eq!(repl_digits("13oneight"), 18);
}
#[test]
fn test_part2() {
assert_eq!(part2("ad1f3").unwrap(), 13);
assert_eq!(part2("ad1\na3\n11\n0").unwrap(), 11 + 33 + 11 + 0);
assert_eq!(part2("zero").unwrap(), 0);
assert_eq!(part2("three").unwrap(), 33);
assert_eq!(part2("1\nthree\nonetwothree2three").unwrap(), 11 + 33 + 13);
assert_eq!(
part2(
r"two1nine
eightwothree
abcone2threexyz
xtwone3four
4nineeightseven2
zoneight234
7pqrstsixteen"
)
.unwrap(),
281
);
}
}
cargo test
gives us a green light, and indeed the solution is correct according to the AoC portal.
Yay, day 1 is done!
One of the things I’ve been doing for quite a while, other than tracking my expenses, has been keeping my CV up-to-date as much as I could. Every three to four months I try to sit down and give a little refresher to my curriculum so that I don’t have to do it in a single take when the time comes.
It goes without saying that this has been quite helpful, I remember a few years ago when this was none of my concern and I ended up spending nights staring at the monitor thinking about what I could write down that was remarkable and that made me stand out. I can’t even remember what I did last week, imagine thinking about something original that I had done in previous months or even years.
I never quite liked my previous CV, maybe because I always get bored with stuff after a while, maybe because I sent mine out to friends and they all copied the design of it, maybe because I copied the design itself from a template that was public on Github under those "awesome-X" repos with thousands of starts.
The other day I fixated on the fact that I wanted to change the design of my CV, remove useless stuff and flatten it just the right amount to make me like it again.
I was dedicated to do it on my own, but there was a thing standing in my way: LaTex.
As many others do, I wrote my first CV and all the other iterations of it on Overleaf using LaTex. I’m no LaTex expert, and I don’t feel like learning the language can be of any use for what I do day to day.
Up until now I was ok with it. I grabbed that template and filled it with some text, the rest has been a combination of magic, luck and mostly try-jacking-stuff-around-until-it-looks-good. This is what it feels like even to this day to me when I think about writing LaTex for such a simple task as updating a CV.
Not knowing LaTex and creating a template from scratch to make it look exactly as I wanted doesn’t sound like a good idea, does it?
While I was thinkering about a possible escape from all of this, I had a flashback of a time when I heard about a good-enough alternative to LaTex and I immediately started to look for it.
It took a couple of tries on Google, and the solution to my problems was lying there: Typst.
Typst sells itself with this line
Typst is a new markup-based typesetting system that is designed to be as powerful as LaTeX while being much easier to learn and use
Sounds pretty cool, right? I immediately started digging in their web based editor and tried writing something out to see if it really was easier and as powerful as LaTex and boy I was not disappointed.
Let me start by saying that I’m in no way trying to say that LaTex is bad, especially because I don’t know how to properly use it, at all. I’m sure LaTex gained its reputation for a reason and it’s there to stay.
But I also don’t know Typst, so I guess I can give at least a fair comparison of the two from the point of view of a guy who doesn’t want to learn a new language to get an average CV in PDF format out of it.
A thing I’ve noticed from the get-go is how fast the PDF is rendered on typst compared to its Overleaf counterpart. As of now, my project takes a whopping four seconds on Overleaf to compile and render on screen. On typst, on the other hand, the same CV renders immediately with no noticable delay. I want to play the devil’s advocate and I think this might be related to Overleaf’s free plan, which I am using, so I don’t want to jump to conclusions on this. Also, on typst everything is layed out in a single file that is 180 lines long, on Overleaf I have a more complex project structure:
$ cd overleaf-cv
$ tree
.
├── awesome-cv.cls
├── fonts
│ ├── FontAwesome.ttf
│ ├── Roboto-Bold.ttf
│ ├── Roboto-BoldItalic.ttf
│ ├── Roboto-Italic.ttf
│ ├── Roboto-Light.ttf
│ ├── Roboto-LightItalic.ttf
│ ├── Roboto-Medium.ttf
│ ├── Roboto-MediumItalic.ttf
│ ├── Roboto-Regular.ttf
│ ├── Roboto-Thin.ttf
│ └── Roboto-ThinItalic.ttf
├── resume
│ ├── competencies.tex
│ ├── education.tex
│ ├── experience.tex
│ ├── open-source-contributions.tex
│ ├── projects.tex
│ ├── skills.tex
│ └── summary.tex
└── resume.tex
$ tokei .
===============================================================================
Language Files Lines Code Comments Blanks
===============================================================================
TeX 8 213 133 50 30
VB6 1 607 548 0 59
===============================================================================
Total 9 820 681 50 89
===============================================================================
That’s a pretty big difference I have to say, and that is the second thing that I immediately noticed. We have a LaTex project that is almost 820 lines long, that I mostly did not write luckily, and a 180 lines long project. Again, to play the devil’s advocate I have to point out that my 180 lines long typst CV does not look exactly like the Overleaf one, so there may be someone else out there that can replicate the design of my typst CV in less than 180 lines in LaTex, so I’ll give you the benefits of the doubt here.
Let’s get down to business and let me talk about what I really care and like
about typst that you won’t find in LaTex in my opinion, and that is simplicity
and a friendly syntax that you might be already familiar with. By reading some
articles online, I agree with the fact that typst syntax looks a lot like
markdown. Indeed, if you don’t need anything fancy you may as well just write
markdown syntax and the end result is going to be almost exactly to what you
would expect from a README.md file on Github. That is because typst is a markup
language as they explain on their reference page. This is a
huge plus to start-off as you don’t need to fill the document with \textbf{}
,
\emph{}
, \textit{}
etc. and if you’re used to markdown files as I am
you have one less thing to worry about here, you can get to a pretty good
result with just that.
CVs have a lot of repetitiveness in themselves, think about it. You usually have an experiences section that has to layout the company you worked for, when you started working there, when you finished and a list of things you did there. It’s easier to reason with variables in this case, indeed you may want to define how you want one experience to look like and just make a for loop over your array of experiences and repeatedly render those one after another. This is possible both in LaTex and typst and it’s called templating. You can see templating as a function that takes in arguments and returns text in this case.
I have a section under my name and surname where I list all the important links that I want to be on my CV. I would like to define a template for a single link that take in:
a URL
a title
an icons
This how you would do it in typst (follow here for better syntax highlight)
// This is an array of links
#let links = (
(icon: "email", link: "mailto:mattiarighetti@outlook.com"),
(icon: "website", link: "https://mattrighetti.com/", display: "mattrighetti.com"),
(icon: "github", link: "https://github.com/mattrighetti", display: "mattrighetti"),
(icon: "linkedin", link: "https://linkedin.com/in/mattia-righetti", display: "mattia-righetti")
)
Sorry for the broken syntax highlight, it's yet not available with highlight.js. By the way, doesn't that look simple? Very tuple-like
Let’s go through the template now which will take in that array and render it
// 1. The icon template will render each link's icon
// The template takes 2 arguments, the latter is 1.5pt by default
#let icon(name, shift: 1.5pt) = {
// box is an inline-container that sizes content I want the icon to be 10pt
// high and the icon itself is under "icons/name.svg"
box(
baseline: shift,
height: 10pt,
image("icons/" + name + ".svg")
)
// I want the icon to have some horizontal padding so that the text of the
// link is not going to be immediately sticked to the its side
h(3pt)
}
#let linksBar(linkarray) = {
// set the text after this statement to be 8pt in size
set text(8pt)
// override the icon shift arg to 2.5pt
let icon = icon.with(shift: 2.5pt)
// iterate over the array
// this is the equivalent of a for loop
linkarray.map(l => {
// render the icon using the previous template
icon(l.icon)
// render the link text
if "display" in l.keys() {
// this is the equivalent of [$l.display](l.link) in markdown
// you'll see square brackets a lot in typst, they are used as text
// containers
link(l.link)[#{l.display}]
} else {
// my email is the only field that does not have a display field
// but the link function will automatically display what's after `mailto:`
link(l.link)
}
})
// we want to render these links horizontally
// so we join all of them together and pad them horizontally
.join(h(10pt))
}
// actually use what we wrote above to render content
#linksBar(links)
If you want to check out the result you can open this on typst.
This will render an horizontal list of links with their respective icons. I don’t know if you feel the same, but I learned all of this in three minutes by looking at the typst’s tutorial which only covers essentials. When I tried to do the same in LaTex I had to Google a lot before finding what worked for me.
I don’t want to transform this article in a tutorial on typst so I’m gonna stop here with the examples but I hope I’ve conveyed the idea of how simple this is compared to LaTex for this specific scenario.
The last and final selling point for me is how simple it is to get from a typst document to a final PDF file. Since typst is written in Rust you can download it from source and compile it into a binary file that you can use to create your PDF file. If you are as lazy as me you can just
$ brew install typst
$ typst compile cv.typ
$ tree
.
├── cv.pdf
├── cv.typ
└── icons
This is how trivial it is to get a PDF out of typst! Last time I tried to setup LaTex on my mac I just gave up because it was too much work, this seems to be agreed upon a lot of other users too so I don’t feel alone on this, am I?
I am in the process of integrating the generation and publishing of my CV
through the same github action that I trigger every time I post an article on my
website. That would save me some extra time because I could just add my .typ
file in my webiste’s github repo, edit the document there in the future and
everything will be published automatically.
I hope I conviced you that you definitely don’t need to write your CV in LaTex, this is a better, simpler and faster solution that just works and that’s gaining popularity even for much more complex documents.
In an effort to improve current resource utilisation and optimisation, the Tor team is developing a new version of their pipeline (v2.0). This update involves transferring much of the data related to Tor nodes and bridges from files stored on a single server’s disk to two separate databases: Postgres and Victoria Metrics.
The main objective of this project is to design a RESTful API service using the
actix_web
framework that is going to be integrated in the new pipeline v2.0 to
support data retrival from the two databases. In particular, the focus will be
on designing the new APIs, its requests, and response formats. The project will
also involve defining appropriate endpoints and data models, ensuring
scalability, performance, and security. The final goal is to achieve a web
service that is going to extend/replace the current onionoo protocol used by
stakeholders interested in the status of the Tor network and its individual
nodes.
Setup CI pipeline for automatic testing
Developed /summary
endpoint
Developed /bandwidth
endpoint
Developed /weights
ednpoint
Developed /clients
endpoint
Developed /details
endpoint
Setup initial integration testing for all endpoints
The greates achievement of our project thus far is the successful internal deployment of the API service. It is currently undergoing rigorous testing and performance testing to validate its functionality and reliability. This testing phase is crucial to ironing out any remaining issues and ensuring a seamless transition to the new pipeline v2.0.
Add meta endpoints to check service health and other useful info
Add benchmarking tests
Add more integration tests
Work on the project’s Wiki
Document Network Status APIs code