This is a collection of compiled solutions for Advent of Code 2021 as the puzzles were solved originally. The solutions are written in Rust.
I have decided to use this opportunity to learn about the Rust programming language.
Since I hope to learn something new with each puzzle, I'm keeping all solutions mostly untouched after the result submission.
This will allow me to consider the progress I made between each puzzle.
Since I might get new insights or ideas for an old solution later down the road, I'll keep them in a separate file, snippets.rs
.
- Reading a text file into a
String
:std::fs::read_to_string
- Iterator over the lines in a
String
:s.lines()
- Map a function/closure over an iterator, discard failures:
it.filter_map(f)
- Type hint only for outer type parameter:
let y: Vec<_> = x.split(" ").collect();
let y = x.split(" ").collect::Vec<_>();
- Chain fallible operations with
and_then
:y[1].parse::<i32>().ok().and_then(|n| y[0].chars().nth(0).and_then(|c| Some((c, n))))
if
statement as ternary operator:counts[i] += if chars.next() == Some('1') { 1 } else { -1 }
- Parse numbers with different bases:
i32::from_str_radix("1011101", 2).unwrap()
- Ranges can be used as iterators:
(0..5).map(|x| 2 * x - 1).collect::<Vec<_>>()
- Blocks can be given labels and broken out of (see
day_04
) - Variables can be declared without type and value if they're not going to be read before first assignment:
let a; a = 3 * something;
- Large
Vec
initialization using a macro:vec![vec![0, 1000]; 1000]
- Argument destructuring in closures:
(&lines).into_iter().filter(|(a,b,c,d)| a == c || b == d)
- Negative numbers can't easily be added to
usize
:if dx * dy > 0 { y += 1; } else { y -= 1; }
- Difference between
into_iter
,iter
anditer_mut
- Remove whitespace around String:
trim
- Custom max function on an iterator:
max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
- Iterate over elements in nested iterators:
flat_map
- Shorten
map
andfilter map
:it.map(f: x -> Result<y, _>).filter_map(Result::ok)
→it.flat_map(f)
Result
is an iterator that yields one value ifOk(x)
and no values ifErr(e)
- Set operations using
HashSet
:intersection
, equality sort
vs.sort_unstable
iter.position(x)
vec.swap(i, j)
iter1.zip(iter2)
- Parse
char
into its digit value:c.to_digit(basis).unwrap()
- Closures cannot be called recursively
- Only consider first
k
elements of an iterator:iter.take(k)
- Extend existing
vec
with and iterator:v.extend(iter)
- Labels let you break out of nested loops:
'outer: for i in ... { ... break 'outer; ... }
- Anonymous values can be used in function calls that need a reference:
f(&mut HashMap::new())
- String slices can be converted into owned strings directly:
"foo".to_owned()
- Closure parameters can't be easily given lifetimes
- Hash maps/sets can take
&str
s as keys String
s can be constructed fromchar
arrays:String::from_iter([key.chars().nth(0).unwrap(), *c])
- also works by collecting:
vec_of_chars.into_iter().collect::<String>()
- also works by collecting:
- Iterating over a changing container:
while let Some(x) = container.pop() { ... }
- Multiple elements for a
Vec
cannot be borrowed if one borrow is mutable - A queue:
let mut queue = VecDeque; queue.push_back(x); queue.pop_front()
- Closure can take ownership of values using
move
:let a = 3; let c = move || a + 3;
- Strings can be formatted using format strings (see
std::fmt
) - Functions that would consume an iterator can instead only take elements by passing
it.by_ref()
:- Take 5 elements into a
vec
:it.by_ref().take(5).collect::<Vec<_>>()
- Take 5 elements into a
- Type trait's associated types can be further bounded:
fn evaluate<I: Iterator<Item = u8>>(...) ...
ž
- Rust doesn't have named tuples, structs must be used
- Binary operators can be implemented for custom types by implementing traits in
std::ops
- Garbage collection can be emulated using a reference-counting pointer:
std::rc::Rc
- Structs/Enums can't have nested types defined inside
- Enum variants can be brought into scope:
enum Foo { A(i32), B, ... }; use Foo::*;
- Debug strings can be derived or implemented by hand for new types
- Dereferenced values can be borrowed:
let (l, r) = &**p;
- Functions can be implemented to extract enum variants directly:
impl Foo { fn a(self) -> i32 { if let Foo::A(n) = self { n } else { panic!("Foo not A") } } }
- Ranges can be exclusive (
a..b
) or inclusive (a..=b
) at the end
- If the size is known in advance:
let a: [i32; 3] = vec.as_slice().try_into().unwrap();
- Destructuring assignments:
let x, y, z; let a = [1, 2, 3]; [x, y, z] = a;
- At the time of writing an unstable features, to be added in Rust 1.59.0
vec
macro can be used to initialize large vecs:let a = vec![0; 1024];
- Useful iterator functions:
std::iter::once(x)
returnsx
oncestd::iter::repeat(x)
returnsx
foreverit1.chain(it2)
first returns all values fromit1
and then all values fromit2
it1.zip(it2)
returns an iterator returning tuples(x1, x2)
while both iterators return valuesit.cycle()
repeatedly returns all values fromit
, repeating from the beginningit.enumerate()
returns enumerated elements fromit
it.flatten()
returns elements of elements ofit
it.fold(init, f)
reducesit
byacc = init; loop { acc = f(acc, it.next()) }
it.intersperse(x)
alternates between elements ofit
and returningx
it.{max,min}_by(f)
appliesf
to each element and returns the extremeit.nth(n)
discardsn - 1
elements and returnsn
thit.reduce(f)
reduces consecutive elements byx = f(x, y)
it.skip(n)
returns an iterator with elements ofit
fromn
onwardit.take(n)
returns nextn
values fromit
(consuming it in the process)it.unzip()
collects tuples into a tuple of containers
- Generic
impl
for genericstruct
:struct Foo<T> { ... }; impl<T> Foo<T> { ... }
- Tuple-like structs:
struct Foo(i32, i32, i32);
- Flush
stdout
:use std::io::{self, Write}; io::stdout().flush().unwrap();
- Sometimes it's worth trying solving the problem by hand.