Skip to content

Commit 2c1ddca

Browse files
committed
feat: aoc 2022 day 13 solution + tests
1 parent ac9e0a2 commit 2c1ddca

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed

_2022/solutions/day13.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
"""Day 13: Distress Signal
2+
3+
This module provides the solution for Advent of Code 2022 - Day 13.
4+
5+
It implements recursive comparison of nested list structures to determine
6+
correct packet ordering based on specific comparison rules.
7+
8+
The module contains a Solution class that inherits from SolutionBase for
9+
parsing and comparing packet data to decode a distress signal.
10+
"""
11+
12+
from ast import literal_eval
13+
from functools import cmp_to_key
14+
from typing import Any
15+
16+
from aoc.models.base import SolutionBase
17+
18+
19+
class Solution(SolutionBase):
20+
"""Compare and sort nested list packets to decode distress signal.
21+
22+
This solution implements custom comparison logic for nested lists and integers
23+
following specific ordering rules. Part 1 identifies pairs already in correct
24+
order and sums their indices. Part 2 sorts all packets with divider packets
25+
and calculates the decoder key.
26+
27+
The comparison rules handle integers, lists, and mixed types with automatic
28+
type conversion when comparing integers against lists.
29+
"""
30+
31+
def _parse_group(self, group: str) -> tuple[list, list]:
32+
"""Parse a pair of packet strings into list structures.
33+
34+
Uses literal_eval to safely parse bracket-delimited packet notation
35+
into Python list structures.
36+
37+
Args:
38+
group: String containing two packet lines separated by newline
39+
40+
Returns
41+
-------
42+
tuple[list, list]: Left and right packet as parsed list structures
43+
"""
44+
left, right = group.strip().split("\n")
45+
return literal_eval(left), literal_eval(right)
46+
47+
def compare(self, left: Any, right: Any) -> int:
48+
"""Compare two packet values recursively following distress signal rules.
49+
50+
Comparison rules:
51+
- Both integers: compare numerically
52+
- Both lists: compare element-by-element, then by length
53+
- Mixed types: convert integer to single-element list and retry
54+
55+
Args:
56+
left: Left packet value (int or list)
57+
right: Right packet value (int or list)
58+
59+
Returns
60+
-------
61+
int: -1 if left < right (correct order), 0 if equal (continue),
62+
1 if left > right (incorrect order)
63+
"""
64+
if isinstance(left, int) and isinstance(right, int):
65+
if left < right:
66+
return -1
67+
68+
if left > right:
69+
return 1
70+
71+
return 0
72+
73+
if isinstance(left, list) and isinstance(right, list):
74+
for idx in range(min(len(left), len(right))):
75+
result = self.compare(left[idx], right[idx])
76+
if result != 0:
77+
return result
78+
79+
if len(left) < len(right):
80+
return -1
81+
82+
if len(left) > len(right):
83+
return 1
84+
85+
return 0
86+
87+
if isinstance(left, int):
88+
return self.compare([left], right)
89+
90+
return self.compare(left, [right])
91+
92+
def part1(self, data: str) -> int:
93+
"""Find sum of indices of packet pairs already in correct order.
94+
95+
Examines each pair of packets and identifies which are already correctly
96+
ordered according to the distress signal comparison rules. Returns the
97+
sum of 1-based indices for pairs in correct order.
98+
99+
Args:
100+
data: Raw input string with packet pairs separated by blank lines
101+
102+
Returns
103+
-------
104+
int: Sum of 1-indexed pair numbers that are already in correct order
105+
"""
106+
score = 0
107+
108+
for idx, group in enumerate(data.split("\n\n"), start=1):
109+
left, right = self._parse_group(group)
110+
111+
if self.compare(left, right) == -1:
112+
score += idx
113+
114+
return score
115+
116+
def part2(self, data: str) -> int:
117+
"""Calculate decoder key by sorting packets with divider packets.
118+
119+
Adds two divider packets [[2]] and [[6]] to all packets, sorts them
120+
using the distress signal comparison rules, and calculates the decoder
121+
key as the product of the 1-based indices of the divider packets.
122+
123+
Args:
124+
data: Raw input string with packet pairs separated by blank lines
125+
126+
Returns
127+
-------
128+
int: Decoder key (product of divider packet indices after sorting)
129+
"""
130+
packets: list[Any] = [[[2]], [[6]]]
131+
for group in data.split("\n\n"):
132+
left, right = self._parse_group(group)
133+
packets.append(left)
134+
packets.append(right)
135+
136+
packets.sort(key=cmp_to_key(self.compare))
137+
return (packets.index([[2]]) + 1) * (packets.index([[6]]) + 1)
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[1,1,3,1,1]
2+
[1,1,5,1,1]
3+
4+
[[1],[2,3,4]]
5+
[[1],4]
6+
7+
[9]
8+
[[8,7,6]]
9+
10+
[[4,4],4,4]
11+
[[4,4],4,4,4]
12+
13+
[7,7,7,7]
14+
[7,7,7]
15+
16+
[]
17+
[3]
18+
19+
[[[]]]
20+
[[]]
21+
22+
[1,[2,[3,[4,[5,6,7]]]],8,9]
23+
[1,[2,[3,[4,[5,6,0]]]],8,9]
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[1,1,3,1,1]
2+
[1,1,5,1,1]
3+
4+
[[1],[2,3,4]]
5+
[[1],4]
6+
7+
[9]
8+
[[8,7,6]]
9+
10+
[[4,4],4,4]
11+
[[4,4],4,4,4]
12+
13+
[7,7,7,7]
14+
[7,7,7]
15+
16+
[]
17+
[3]
18+
19+
[[[]]]
20+
[[]]
21+
22+
[1,[2,[3,[4,[5,6,7]]]],8,9]
23+
[1,[2,[3,[4,[5,6,0]]]],8,9]

_2022/tests/test_13.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Test suite for Day 13: Distress Signal
2+
3+
This module contains tests for the Day 13 solution, which compares and sorts
4+
nested list packets to decode a distress signal. The tests verify:
5+
6+
1. Part 1: Finding sum of indices for packet pairs in correct order
7+
2. Part 2: Calculating decoder key by sorting packets with dividers
8+
"""
9+
10+
from aoc.models.tester import TestSolutionUtility
11+
12+
13+
def test_day13_part1() -> None:
14+
"""Test finding sum of correctly ordered packet pair indices.
15+
16+
This test runs the solution for Part 1 of the puzzle against the
17+
provided test input and compares the result with the expected output.
18+
"""
19+
TestSolutionUtility.run_test(
20+
year=2022,
21+
day=13,
22+
is_raw=True,
23+
part_num=1,
24+
expected=13,
25+
)
26+
27+
28+
def test_day13_part2() -> None:
29+
"""Test calculating decoder key from sorted packets.
30+
31+
This test runs the solution for Part 2 of the puzzle against the
32+
provided test input and compares the result with the expected output.
33+
"""
34+
TestSolutionUtility.run_test(
35+
year=2022,
36+
day=13,
37+
is_raw=True,
38+
part_num=2,
39+
expected=140,
40+
)

0 commit comments

Comments
 (0)