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 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
//! This is my solution for [Advent of Code - Day 9: _Mirage Maintenance_](https://adventofcode.com/2023/day/9)
//!
//! [`parse_input`] uses [`parse_line`] to interpret the puzzle input as sequences of integers.
//!
//! [`analyse_sequences`] hands analysing each sequence to the provided extrapolator, either
//! [`extrapolate_sequence_forwards`] for part 1 or [`extrapolate_sequence_backwards`] for part 2.
//!
//! Both extrapolators rely on [`build_delta_sequences`], which uses [`iterate`] to recursively generate
//! the a sequence of sequences, each sequence in turn being generated by [`build_delta_sequence`]
//! from the previous sequence.
use itertools::{iterate, Itertools};
use std::fs;
/// The entry point for running the solutions with the 'real' puzzle input.
///
/// - The puzzle input is expected to be at `<project_root>/res/day-9-input`
/// - It is expected this will be called by [`super::main()`] when the user elects to run day 9.
pub fn run() {
let contents = fs::read_to_string("res/day-9-input.txt").expect("Failed to read file");
let sequences = parse_input(&contents);
println!(
"The sum of the forwards extrapolated numbers is: {}",
analyse_sequences(&sequences, extrapolate_sequence_forwards)
);
println!(
"The sum of the backwards extrapolated numbers is: {}",
analyse_sequences(&sequences, extrapolate_sequence_backwards)
);
}
/// Parse each line as a sequence of integers
fn parse_input(input: &String) -> Vec<Vec<i64>> {
input.lines().map(parse_line).collect()
}
/// Parse a line as a space separated list of integers
fn parse_line(line: &str) -> Vec<i64> {
line.split(" ").filter_map(|num| num.parse().ok()).collect()
}
/// Unwrap the sequence of delta sequences to extrapolate the next value in the original sequence.
/// This is equivalent to the sum of the last value in each of the delta sequences
fn extrapolate_sequence_forwards(sequence: &Vec<i64>) -> i64 {
let sequences: Vec<Vec<i64>> = build_delta_sequences(sequence);
sequences
.iter()
.rev()
.map(|seq| seq.last().unwrap_or(&0))
.sum()
}
/// Unwrap the sequence of delta sequences to extrapolate the previous value in the original
/// sequence. This is equivalent recursively subtracting the first value in the sequence.
fn extrapolate_sequence_backwards(sequence: &Vec<i64>) -> i64 {
let sequences: Vec<Vec<i64>> = build_delta_sequences(sequence);
sequences
.iter()
.rev()
.map(|seq| seq.first().unwrap_or(&0))
.fold(0, |acc, val| val - acc)
}
/// Given a list of integers, return the sequence of sequences generated by recursively calling
/// [`build_delta_sequence`] on the previous sequence until a sequence of `0`s is generated
fn build_delta_sequences(sequence: &Vec<i64>) -> Vec<Vec<i64>> {
iterate(sequence.clone(), build_delta_sequence)
.take_while(|seq| seq.iter().any(|&v| v != 0))
.collect()
}
/// Generate a sequence of the difference between each consecutive pair of numbers
fn build_delta_sequence(sequence: &Vec<i64>) -> Vec<i64> {
sequence
.into_iter()
.tuple_windows()
.map(|(a, b)| b - a)
.collect()
}
/// Extrapolate each sequence in the list, and sum the extrapolated values
fn analyse_sequences(
sequences: &Vec<Vec<i64>>,
extrapolator: fn(sequence: &Vec<i64>) -> i64,
) -> i64 {
sequences.iter().map(extrapolator).sum()
}
#[cfg(test)]
mod tests {
use crate::day_9::*;
fn example_sequences() -> Vec<Vec<i64>> {
vec![
vec![0, 3, 6, 9, 12, 15],
vec![1, 3, 6, 10, 15, 21],
vec![10, 13, 16, 21, 30, 45],
]
}
#[test]
fn can_parse_input() {
let input = "\
0 3 6 9 12 15
1 3 6 10 15 21
10 13 16 21 30 45"
.to_string();
assert_eq!(parse_input(&input), example_sequences());
}
#[test]
fn can_extrapolate_sequences_forwards() {
let results: Vec<i64> = example_sequences()
.iter()
.map(extrapolate_sequence_forwards)
.collect();
assert_eq!(results, vec![18, 28, 68])
}
#[test]
fn can_extrapolate_sequences_backwards() {
let results: Vec<i64> = example_sequences()
.iter()
.map(extrapolate_sequence_backwards)
.collect();
assert_eq!(results, vec![-3, 0, 5])
}
}