advent of code 2025 in ts and nix
1let
2 input = builtins.readFile ../../shared/05/input.txt;
3
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]
8
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:
12 let
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; }
17 ) rangeLines;
18
19 # Sort ranges by start position using merge sort
20 sortedRanges =
21 let
22 merge = left: right:
23 let
24 mergeImpl = l: r: acc:
25 if builtins.length l == 0 then acc ++ r
26 else if builtins.length r == 0 then acc ++ l
27 else
28 let
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 [];
37
38 mergeSort = list:
39 let len = builtins.length list;
40 in if len <= 1 then list
41 else
42 let
43 mid = len / 2;
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;
48
49 # Merge overlapping or adjacent ranges
50 mergedRanges =
51 let
52 merge = builtins.foldl' (state: range:
53 if state.current == null then { current = range; result = []; }
54 else
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; }
60 else
61 # Gap - save current and start new
62 { current = range;
63 result = state.result ++ [curr]; }
64 ) { current = null; result = []; } sortedRanges;
65 in if merge.current == null then merge.result else merge.result ++ [merge.current];
66
67 # Parse ingredients
68 ingredientLines = builtins.filter (s: builtins.isString s && s != "") (builtins.split "\n" ingredientsSection);
69 ingredients = builtins.map (line: builtins.fromJSON line) ingredientLines;
70
71 # Sort ingredients using merge sort
72 sortedIngredients =
73 let
74 merge = left: right:
75 let
76 mergeImpl = l: r: acc:
77 if builtins.length l == 0 then acc ++ r
78 else if builtins.length r == 0 then acc ++ l
79 else
80 let
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 [];
89
90 mergeSort = list:
91 let len = builtins.length list;
92 in if len <= 1 then list
93 else
94 let
95 mid = len / 2;
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;
100
101 # Part 1: Count ingredients that fall within merged ranges
102 part1 =
103 let
104 # Use foldl' to process ingredients with range index tracking
105 result = builtins.foldl' (state: ingredient:
106 let
107 # Find the appropriate range starting from current index
108 findRange = rangeIdx:
109 if rangeIdx >= builtins.length mergedRanges then { found = false; newIdx = rangeIdx; }
110 else
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);
115
116 rangeResult = findRange state.rangeIdx;
117 in {
118 count = state.count + (if rangeResult.found then 1 else 0);
119 rangeIdx = rangeResult.newIdx;
120 }
121 ) { count = 0; rangeIdx = 0; } sortedIngredients;
122 in result.count;
123
124 # Part 2: Total coverage of all merged ranges
125 part2 = builtins.foldl' (sum: range: sum + (range.end - range.start + 1)) 0 mergedRanges;
126
127in {
128 inherit part1 part2;
129}