···
2
+
input = builtins.readFile ../../shared/05/input.txt;
4
+
# Split input into two sections
5
+
sections = builtins.filter (s: s != "") (builtins.split "\n\n" input);
6
+
rangesSection = builtins.head sections;
7
+
ingredientsSection = builtins.elemAt sections 2; # sections are [ranges, "\n\n", ingredients]
9
+
# Parse ranges from "start-end" format
10
+
rangeLines = builtins.filter (s: builtins.isString s && s != "") (builtins.split "\n" rangesSection);
11
+
parsedRanges = builtins.map (line:
13
+
parts = builtins.filter (s: builtins.isString s && s != "") (builtins.split "-" line);
14
+
start = builtins.fromJSON (builtins.head parts);
15
+
end = builtins.fromJSON (builtins.elemAt parts 1);
16
+
in { inherit start end; }
19
+
# Sort ranges by start position using merge sort
22
+
merge = left: right:
24
+
mergeImpl = l: r: acc:
25
+
if builtins.length l == 0 then acc ++ r
26
+
else if builtins.length r == 0 then acc ++ l
29
+
lHead = builtins.head l;
30
+
rHead = builtins.head r;
31
+
lTail = builtins.tail l;
32
+
rTail = builtins.tail r;
33
+
in if lHead.start <= rHead.start
34
+
then mergeImpl lTail r (acc ++ [lHead])
35
+
else mergeImpl l rTail (acc ++ [rHead]);
36
+
in mergeImpl left right [];
39
+
let len = builtins.length list;
40
+
in if len <= 1 then list
44
+
left = builtins.genList (i: builtins.elemAt list i) mid;
45
+
right = builtins.genList (i: builtins.elemAt list (i + mid)) (len - mid);
46
+
in merge (mergeSort left) (mergeSort right);
47
+
in mergeSort parsedRanges;
49
+
# Merge overlapping or adjacent ranges
52
+
merge = builtins.foldl' (state: range:
53
+
if state.current == null then { current = range; result = []; }
55
+
let curr = state.current;
56
+
in if curr.end + 1 >= range.start then
57
+
# Overlapping or adjacent - merge
58
+
{ current = { start = curr.start; end = if curr.end > range.end then curr.end else range.end; };
59
+
result = state.result; }
61
+
# Gap - save current and start new
63
+
result = state.result ++ [curr]; }
64
+
) { current = null; result = []; } sortedRanges;
65
+
in if merge.current == null then merge.result else merge.result ++ [merge.current];
68
+
ingredientLines = builtins.filter (s: builtins.isString s && s != "") (builtins.split "\n" ingredientsSection);
69
+
ingredients = builtins.map (line: builtins.fromJSON line) ingredientLines;
71
+
# Sort ingredients using merge sort
74
+
merge = left: right:
76
+
mergeImpl = l: r: acc:
77
+
if builtins.length l == 0 then acc ++ r
78
+
else if builtins.length r == 0 then acc ++ l
81
+
lHead = builtins.head l;
82
+
rHead = builtins.head r;
83
+
lTail = builtins.tail l;
84
+
rTail = builtins.tail r;
85
+
in if lHead <= rHead
86
+
then mergeImpl lTail r (acc ++ [lHead])
87
+
else mergeImpl l rTail (acc ++ [rHead]);
88
+
in mergeImpl left right [];
91
+
let len = builtins.length list;
92
+
in if len <= 1 then list
96
+
left = builtins.genList (i: builtins.elemAt list i) mid;
97
+
right = builtins.genList (i: builtins.elemAt list (i + mid)) (len - mid);
98
+
in merge (mergeSort left) (mergeSort right);
99
+
in mergeSort ingredients;
101
+
# Part 1: Count ingredients that fall within merged ranges
104
+
# Use foldl' to process ingredients with range index tracking
105
+
result = builtins.foldl' (state: ingredient:
107
+
# Find the appropriate range starting from current index
108
+
findRange = rangeIdx:
109
+
if rangeIdx >= builtins.length mergedRanges then { found = false; newIdx = rangeIdx; }
111
+
let range = builtins.elemAt mergedRanges rangeIdx;
112
+
in if ingredient < range.start then { found = false; newIdx = rangeIdx; }
113
+
else if ingredient <= range.end then { found = true; newIdx = rangeIdx; }
114
+
else findRange (rangeIdx + 1);
116
+
rangeResult = findRange state.rangeIdx;
118
+
count = state.count + (if rangeResult.found then 1 else 0);
119
+
rangeIdx = rangeResult.newIdx;
121
+
) { count = 0; rangeIdx = 0; } sortedIngredients;
124
+
# Part 2: Total coverage of all merged ranges
125
+
part2 = builtins.foldl' (sum: range: sum + (range.end - range.start + 1)) 0 mergedRanges;
128
+
inherit part1 part2;