
As the forklifts break through the wall, the Elves are delighted to discover that there was a cafeteria on the other side after all.
Solution in Java
Full source can be found: in GitHub
Day 5 revolves around interpreting ranges of ingredient IDs and determining which IDs are considered fresh. The input is split into two logical sections: a list of inclusive ID ranges, followed by a list of individual ingredient IDs. Part one asks us to validate the individual IDs against the ranges, while part two focuses entirely on the ranges themselves.
Reading the data
First step for this assignment is processing the data into structures that can be used in algorithms. To do this I created 2 structures representing the data:
1private record Id(long id) {}
2
3private record IdRange(long min, long max) {
4 public boolean contains(Id id) {
5 return id.id >= min && id.id <= max;
6 }
7
8 public long freshIds() {
9 return max - min + 1;
10 }
11
12 public boolean hasOverlap(IdRange other) {
13 return min <= other.max && max >= other.min;
14 }
15
16 public IdRange merge(IdRange other) {
17 return new IdRange(Math.min(min, other.min), Math.max(max, other.max));
18 }
19}
And the following logic to parse the data into a set of ranges with product IDs.
1@Override
2public void readInput() {
3 ranges = new ArrayList<>();
4 ingredientIds = new ArrayList<>();
5
6 boolean rangesDone = false;
7 Scanner scanner = inputLoader.scanner();
8 while (scanner.hasNextLine()) {
9 String line = scanner.nextLine();
10 if (line.isEmpty()) {
11 rangesDone = true;
12 continue;
13 }
14
15 if (!rangesDone) {
16 String[] bounds = line.split("-");
17 IdRange idRange = new IdRange(Long.parseLong(bounds[0]), Long.parseLong(bounds[1]));
18 ranges.add(idRange);
19 } else {
20 ingredientIds.add(new Id(Long.parseLong(line)));
21 }
22 }
23
24 mergeOverlappingRanges();
25}
26
27private void mergeOverlappingRanges() {
28 boolean hasChanged = true;
29 List<IdRange> original = ranges;
30 List<IdRange> mergedRanges = new ArrayList<>();
31 while (hasChanged) {
32 hasChanged = false;
33 for (IdRange range : original) {
34 Optional<IdRange> overlappingRange = original.stream()
35 .filter(r -> !Objects.equals(r, range) && r.hasOverlap(range)).findFirst();
36 if (overlappingRange.isPresent()) {
37 hasChanged = true;
38 mergedRanges.add(range.merge(overlappingRange.get()));
39 } else {
40 mergedRanges.add(range);
41 }
42 }
43 original = mergedRanges.stream().distinct().toList();
44 mergedRanges = new ArrayList<>();
45 }
46
47 ranges = original;
48}
In this it is important to combine any ranges of the input data with overlaps, as this would otherwise provide an incorrect count in the second part for today. This is done in the mergeOverlappingRanges method, which keeps looping over the data until nothing can be merged.
Part 1 - Counting fresh ingredient IDs
In the first part of the assignment, the task is to determine how many of the listed ingredient IDs fall within at least one of the fresh ID ranges. The ranges may overlap, and an ingredient is considered fresh if it appears in any of them.
The Java solution begins by reading and parsing the input. Ranges and ingredient IDs are stored separately, and once all input has been read, overlapping ranges are merged so later checks are both simpler and more efficient.
The actual logic for part one is concise and expressive. For each ingredient ID, the code checks whether any range contains it and counts how many satisfy that condition.
1@Override
2public void part1() {
3 long nonSpoiled = ingredientIds.stream()
4 .filter(id -> ranges.stream().anyMatch(range -> range.contains(id)))
5 .count();
6
7 validator.part1(nonSpoiled);
8}
This solution uses Java streams to clearly model the problem domain. The outer stream iterates over all ingredient IDs, while the inner stream checks each ID against all known ranges. The contains method on IdRange expresses the inclusive range logic directly, keeping the intent easy to read. Once the count is computed, it is passed to the validator to confirm the correct answer.
Part 2 - Counting all fresh IDs in the ranges
Part two changes perspective. Instead of checking individual ingredient IDs, the question now is how many distinct IDs are considered fresh by the ranges themselves. The second section of the input is ignored entirely.
Because overlapping ranges have already been merged during input processing, each range can safely be treated as a unique, non-overlapping block of fresh IDs. This makes the computation straightforward: calculate how many IDs each range represents and sum them all.
1@Override
2public void part2() {
3 BigInteger nonSpoiled = ranges.stream()
4 .map(range -> BigInteger.valueOf(range.freshIds()))
5 .reduce(BigInteger::add)
6 .get();
7
8 validator.part2(nonSpoiled);
9}
Here, each range contributes a count equal to its inclusive size, calculated by max - min + 1. The use of BigInteger ensures that very large inputs are handled safely without risking overflow. The final sum represents the total number of fresh ingredient IDs implied by the ranges.
A key piece that enables this simplicity is the merging of overlapping ranges during input reading. That logic ensures that part two does not accidentally double-count IDs that appear in multiple overlapping ranges.
Overall, this solution cleanly separates concerns between parsing, normalization of data, and problem-solving logic. By resolving overlaps early and modeling the domain with small, focused record types, both parts of the assignment become easy to reason about and verify.