This document explains a Rust program that validates sequences of numbers and counts valid patterns according to specific criteria. The program examines “reports” containing numeric sequences, validates them against rules, and then explores how removing elements affects validity.
The core problem involves validating sequences of numbers according to specific rules:
This is essentially a pattern recognition problem where we need to:
We represent each numeric sequence as a Report
struct with the following structure:
#[derive(Debug)]
struct Report {
levels: rc::Rc<[usize]>,
}
The Rc<[usize]>
(reference-counted array) is chosen to efficiently handle the sequences with shared ownership. This avoids unnecessary copying of data when manipulating sequences.
The program parses the input file line by line, converting each line into a Report
:
fn main() {
let input = fs::read_to_string("src/bin/day2/input.txt").expect("File not found");
let lists = input
.lines()
.map(|line| line.parse::<Report>().expect("Invalid list"))
.collect::<Vec<Report>>();
// ...
}
We implement the FromStr
trait to allow parsing strings directly into our Report
struct:
impl FromStr for Report {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Report {
levels: s
.split_ascii_whitespace()
.map(|n| n.parse::<usize>())
.collect::<Result<rc::Rc<[usize]>, ParseIntError>>()?
})
}
}
This approach leverages Rust’s trait system to make the code more readable and maintainable, separating the parsing logic from the main program flow.
The core algorithm checks two conditions for each sequence:
fn validate(r: &[usize]) -> bool {
let dir = r[0] < r[1];
r.windows(2)
.all(|a| {
(1..=3).contains(&(a[0].abs_diff(a[1])))
&& match dir {
true => a[0] < a[1],
false => a[0] > a[1],
}
})
}
This function:
windows(2)
method to check consecutive pairs of elementsThe all()
combinator ensures that every pair satisfies both conditions.
For part 1, we simply count the sequences that are already valid:
let count = lists.iter().filter(|r| r.is_safe()).count();
The is_safe()
method is a simple wrapper around our validation function:
fn is_safe(&self) -> bool {
Report::validate(&self.levels)
}
In part 2, we need to determine if removing any single element makes an invalid sequence valid:
fn is_safe_dumpen(&self) -> bool {
(0..self.levels.len()).any(|p| {
let mut levels = self.levels.to_vec();
levels.remove(p);
Report::validate(&levels)
})
}
This function:
We then count the sequences that satisfy this condition:
let count = lists.iter().filter(|r| r.is_safe_dumpen()).count();
The program includes timing code to measure performance:
let t = time::Instant::now();
// code to time
println!("Part 1: {} = {:?}", count, t.elapsed());
Several design choices improve efficiency:
Rc<[usize]>
allows efficient cloning of sequenceswindows(2)
approach avoids manual indexing and bounds checkingany()
combinator in is_safe_dumpen()
short-circuits once a valid modification is foundThis program demonstrates effective use of Rust’s features:
Result
typeRc
The algorithm itself highlights the importance of breaking down complex validation into simpler rules and leveraging Rust’s standard library to express those rules concisely and efficiently.