tangled
alpha
login
or
join now
jonas.tngl.sh
/
advent-of-code
0
fork
atom
Advent of Code solutions in Rust
0
fork
atom
overview
issues
pulls
pipelines
feat: day 5 of AoC 2025
jonas.tngl.sh
3 months ago
4412d3ea
820277bf
1/1
rust.yml
success
3m 21s
+142
-2
2 changed files
expand all
collapse all
unified
split
aoc_2025
src
day05.rs
main.rs
+140
aoc_2025/src/day05.rs
···
1
1
+
use std::{collections::HashSet, ops::RangeInclusive};
2
2
+
3
3
+
use anyhow::Context;
4
4
+
use aoc_companion::prelude::*;
5
5
+
use aoc_utils::range::RangeSet;
6
6
+
use itertools::Itertools as _;
7
7
+
8
8
+
pub(crate) struct Door {
9
9
+
fresh_ranges: Vec<RangeInclusive<u64>>,
10
10
+
available_ingredients: Vec<u64>,
11
11
+
}
12
12
+
13
13
+
impl<'input> Solution<'input> for Door {
14
14
+
fn parse(input: &'input str) -> Result<Self> {
15
15
+
let Some((fresh, available)) = input.split_once("\n\n") else {
16
16
+
anyhow::bail!("missing empty line separating fresh ranges from ingredients");
17
17
+
};
18
18
+
Ok(Door {
19
19
+
fresh_ranges: fresh
20
20
+
.lines()
21
21
+
.map(|r| -> Result<_> {
22
22
+
let (from, to) = r
23
23
+
.split_once('-')
24
24
+
.with_context(|| anyhow::anyhow!("missing dash in ID range"))?;
25
25
+
Ok(from.parse()?..=to.parse()?)
26
26
+
})
27
27
+
.try_collect()?,
28
28
+
available_ingredients: available.lines().map(|i| i.parse()).try_collect()?,
29
29
+
})
30
30
+
}
31
31
+
32
32
+
fn part1(&self) -> usize {
33
33
+
fresh_ingredients(&self.available_ingredients, &self.fresh_ranges).count()
34
34
+
}
35
35
+
36
36
+
fn part2(&self) -> usize {
37
37
+
disjoint_ranges(&self.fresh_ranges)
38
38
+
.into_iter()
39
39
+
.map(|r| r.count())
40
40
+
.sum()
41
41
+
}
42
42
+
}
43
43
+
44
44
+
fn fresh_ingredients<'a, I>(
45
45
+
available_ingredients: impl IntoIterator<Item = &'a u64> + 'a,
46
46
+
fresh_ranges: I,
47
47
+
) -> impl Iterator<Item = u64> + 'a
48
48
+
where
49
49
+
I: IntoIterator<Item = &'a RangeInclusive<u64>> + 'a,
50
50
+
I::IntoIter: Clone,
51
51
+
{
52
52
+
let fresh_ranges = fresh_ranges.into_iter();
53
53
+
available_ingredients
54
54
+
.into_iter()
55
55
+
.cloned()
56
56
+
.filter(move |i| fresh_ranges.clone().any(|r| r.contains(i)))
57
57
+
}
58
58
+
59
59
+
fn disjoint_ranges<'a>(
60
60
+
ranges: impl IntoIterator<Item = &'a RangeInclusive<u64>>,
61
61
+
) -> HashSet<RangeInclusive<u64>> {
62
62
+
ranges.into_iter().fold(
63
63
+
HashSet::<RangeInclusive<u64>>::new(),
64
64
+
|mut disjoint_ranges, r| {
65
65
+
let overlapping = disjoint_ranges.extract_if(|d| d.try_union(r).is_some());
66
66
+
let union = overlapping
67
67
+
.map(|o| o.try_union(r).unwrap())
68
68
+
.reduce(|lhs, rhs| lhs.try_union(&rhs).unwrap())
69
69
+
.unwrap_or(r.clone());
70
70
+
disjoint_ranges.insert(union);
71
71
+
disjoint_ranges
72
72
+
},
73
73
+
)
74
74
+
}
75
75
+
76
76
+
#[cfg(test)]
77
77
+
mod tests {
78
78
+
use super::*;
79
79
+
80
80
+
const EXAMPLE_INPUT: &str = "\
81
81
+
3-5
82
82
+
10-14
83
83
+
16-20
84
84
+
12-18
85
85
+
86
86
+
1
87
87
+
5
88
88
+
8
89
89
+
11
90
90
+
17
91
91
+
32";
92
92
+
93
93
+
const EXAMPLE_FRESH_RANGES: &[RangeInclusive<u64>] = &[3..=5, 10..=14, 16..=20, 12..=18];
94
94
+
95
95
+
const EXAMPLE_AVAILABLE_INGREDIENTS: &[u64] = &[1, 5, 8, 11, 17, 32];
96
96
+
97
97
+
#[test]
98
98
+
fn parse_example_input() {
99
99
+
let Door {
100
100
+
fresh_ranges,
101
101
+
available_ingredients,
102
102
+
} = Door::parse(EXAMPLE_INPUT).unwrap();
103
103
+
assert_eq!(fresh_ranges, EXAMPLE_FRESH_RANGES);
104
104
+
assert_eq!(available_ingredients, EXAMPLE_AVAILABLE_INGREDIENTS);
105
105
+
}
106
106
+
107
107
+
#[test]
108
108
+
fn example_fresh_ingredients() {
109
109
+
itertools::equal(
110
110
+
fresh_ingredients(EXAMPLE_AVAILABLE_INGREDIENTS, EXAMPLE_FRESH_RANGES),
111
111
+
[5, 11, 17],
112
112
+
);
113
113
+
}
114
114
+
115
115
+
#[test]
116
116
+
fn disjoint_ranges_cover_same_ids() {
117
117
+
assert_eq!(
118
118
+
disjoint_ranges(EXAMPLE_FRESH_RANGES)
119
119
+
.into_iter()
120
120
+
.flatten()
121
121
+
.collect::<HashSet<_>>(),
122
122
+
EXAMPLE_FRESH_RANGES
123
123
+
.iter()
124
124
+
.cloned()
125
125
+
.flatten()
126
126
+
.collect::<HashSet<_>>()
127
127
+
);
128
128
+
}
129
129
+
130
130
+
#[test]
131
131
+
fn disjoint_ranges_are_mutually_disjoint() {
132
132
+
assert_eq!(
133
133
+
disjoint_ranges(EXAMPLE_FRESH_RANGES)
134
134
+
.iter()
135
135
+
.tuple_combinations()
136
136
+
.find(|(lhs, rhs)| lhs.intersection(rhs).is_some()),
137
137
+
None
138
138
+
);
139
139
+
}
140
140
+
}
+2
-2
aoc_2025/src/main.rs
···
4
4
mod day02;
5
5
mod day03;
6
6
mod day04;
7
7
-
// mod day05;
7
7
+
mod day05;
8
8
// mod day06;
9
9
// mod day07;
10
10
// mod day08;
···
22
22
door!(2025-12-02 ~> day02),
23
23
door!(2025-12-03 ~> day03),
24
24
door!(2025-12-04 ~> day04),
25
25
-
// door!(2025-12-05 ~> day05),
25
25
+
door!(2025-12-05 ~> day05),
26
26
// door!(2025-12-06 ~> day06),
27
27
// door!(2025-12-07 ~> day07),
28
28
// door!(2025-12-08 ~> day08),