Rust
I didn't look at the input at first so tried a naive version with sets, but it was too slow even for part one. I got smarter and wrote a version with less code that runs in under half a millisecond.
type Id = usize;
struct Ingredients {
fresh: Vec<RangeInclusive<Id>>,
available: HashSet<Id>,
}
impl Ingredients {
fn is_fresh(&self, id: Id) -> bool {
self.fresh.iter().any(|range| range.contains(&id))
}
fn num_fresh_available(&self) -> usize {
self.available
.iter()
.filter(|&&id| self.is_fresh(id))
.count()
}
fn num_fresh_all(&self) -> usize {
self.fresh
.iter()
.map(|range| 1 + range.end() - range.start())
.sum()
}
}
fn merge_ranges(mut ranges: Vec<RangeInclusive<Id>>) -> Vec<RangeInclusive<Id>> {
if ranges.is_empty() {
return ranges;
}
ranges.sort_by_key(|range| *range.start());
let mut merged = Vec::new();
let mut start = ranges[0].start();
let mut end = ranges[0].end();
for range in ranges.iter().skip(1) {
if range.start() > end {
merged.push(RangeInclusive::new(*start, *end));
start = range.start();
end = range.end();
}
if range.end() > end {
end = range.end();
}
}
merged.push(RangeInclusive::new(*start, *end));
merged
}
impl FromStr for Ingredients {
type Err = Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((fresh_str, avail_str)) = s.split_once("\n\n") else {
bail!("missing divider");
};
let fresh = fresh_str
.lines()
.map(|l| -> Result<RangeInclusive<_>, Self::Err> {
let (start, end) = l.split_once('-').ok_or_eyre("bad range")?;
let start: Id = start.parse()?;
let end: Id = end.parse()?;
Ok(start..=end)
})
.collect::<Result<_, _>>()?;
let available = avail_str
.lines()
.map(|l| l.parse())
.collect::<Result<_, _>>()?;
Ok(Ingredients {
fresh: merge_ranges(fresh),
available,
})
}
}
::: spoiler Full code
use std::{collections::HashSet, fs, ops::RangeInclusive, str::FromStr};
use color_eyre::eyre::{OptionExt, Report, Result, bail};
#[derive(Debug, PartialEq, Eq)]
struct Ingredients {
fresh: Vec<RangeInclusive<Id>>,
available: HashSet<Id>,
}
type Id = usize;
impl Ingredients {
fn is_fresh(&self, id: Id) -> bool {
self.fresh.iter().any(|range| range.contains(&id))
}
fn num_fresh_available(&self) -> usize {
self.available
.iter()
.filter(|&&id| self.is_fresh(id))
.count()
}
fn num_fresh_all(&self) -> usize {
self.fresh
.iter()
.map(|range| 1 + range.end() - range.start())
.sum()
}
}
fn merge_ranges(mut ranges: Vec<RangeInclusive<Id>>) -> Vec<RangeInclusive<Id>> {
if ranges.is_empty() {
return ranges;
}
ranges.sort_by_key(|range| *range.start());
let mut merged = Vec::new();
let mut start = ranges[0].start();
let mut end = ranges[0].end();
for range in ranges.iter().skip(1) {
if range.start() > end {
merged.push(RangeInclusive::new(*start, *end));
start = range.start();
end = range.end();
}
if range.end() > end {
end = range.end();
}
}
merged.push(RangeInclusive::new(*start, *end));
merged
}
impl FromStr for Ingredients {
type Err = Report;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let Some((fresh_str, avail_str)) = s.split_once("\n\n") else {
bail!("missing divider");
};
let fresh = fresh_str
.lines()
.map(|l| -> Result<RangeInclusive<_>, Self::Err> {
let (start, end) = l.split_once('-').ok_or_eyre("bad range")?;
let start: Id = start.parse()?;
let end: Id = end.parse()?;
Ok(start..=end)
})
.collect::<Result<_, _>>()?;
let available = avail_str
.lines()
.map(|l| l.parse())
.collect::<Result<_, _>>()?;
Ok(Ingredients {
fresh: merge_ranges(fresh),
available,
})
}
}
fn part1(filepath: &str) -> Result<usize> {
let input = fs::read_to_string(filepath)?;
let ingrd = Ingredients::from_str(&input).unwrap();
Ok(ingrd.num_fresh_available())
}
fn part2(filepath: &str) -> Result<usize> {
let input = fs::read_to_string(filepath)?;
let ingrd = Ingredients::from_str(&input).unwrap();
Ok(ingrd.num_fresh_all())
}
fn main() -> Result<()> {
color_eyre::install()?;
println!("Part 1: {}", part1("d05/input.txt")?);
println!("Part 2: {}", part2("d05/input.txt")?);
Ok(())
}