11import re
22
33
4+ class Part :
5+ def __init__ (self , part_number ):
6+ self .part_number = str (part_number )
7+ self .coordinates = []
8+ self .window = None
9+ self .x_start_coordinate = None
10+ self .x_end_coordinate = None
11+ self .y_coordinate = None
12+ self .full_coordinates = None
13+
14+ def __str__ (self ):
15+ return (
16+ "\n "
17+ f"part_number = { self .part_number } \n "
18+ f"coordinates = { self .coordinates } \n "
19+ f"window = { self .window } \n "
20+ f"x_start_coordinate = { self .x_start_coordinate } \n "
21+ f"x_end_coordinate = { self .x_end_coordinate } \n "
22+ f"y_coordinate = { self .y_coordinate } \n "
23+ f"full_coordinates = { self .full_coordinates } \n "
24+ )
25+
26+ def set_coordinates (self , y_pos , x_start_pos , x_end_pos ):
27+ if x_start_pos > x_end_pos :
28+ raise ValueError (
29+ "Invalid coordinates: "
30+ "x_start_pos should be less than or equal to x_end_pos"
31+ )
32+ self .x_start_coordinate = x_start_pos
33+ self .x_end_coordinate = x_end_pos
34+ self .y_coordinate = y_pos
35+ self .full_coordinates = [(y_pos , x ) for x in range (x_start_pos , x_end_pos )]
36+
37+ assert len (self .full_coordinates ) == len (self .part_number )
38+
39+ def part_number_as_integer (self ):
40+ try :
41+ return int (self .part_number )
42+ except (ValueError , TypeError ):
43+ # Handle the case where part_number is not a valid integer
44+ return None
45+
46+ def get_window_around_part (self ):
47+ if self .full_coordinates is None :
48+ raise ValueError (
49+ "Part has no full_coordinates! Check Part initialised correctly."
50+ )
51+
52+ self .window = []
53+ for y in [self .y_coordinate - 1 , self .y_coordinate , self .y_coordinate + 1 ]:
54+ for x in range (self .x_start_coordinate - 1 , self .x_end_coordinate + 1 ):
55+ self .window .append ((y , x ))
56+
57+ def check_window_for_symbol (self , symbol_coordinates ):
58+ return bool (set (self .window ).intersection (symbol_coordinates ))
59+
60+
61+ class Symbol :
62+ def __init__ (self , char , y , x ):
63+ self .symbol = char
64+ self .y = y
65+ self .x = x
66+ self .coordinates = (y , x )
67+ self .is_potential_gear = self .symbol == "*"
68+ self .is_gear = False
69+ self .window = None
70+ self .neighbouring_parts = None
71+ self .ratio = None
72+
73+ def __str__ (self ):
74+ return (
75+ "\n "
76+ f"symbol = { self .symbol } \n "
77+ f"y = { self .y } \n "
78+ f"y = { self .x } \n "
79+ f"x = { self .x } \n "
80+ f"coordinates = { self .coordinates } \n "
81+ f"is_potential_gear = { self .is_potential_gear } \n "
82+ f"is_gear = { self .is_gear } \n "
83+ f"window = { self .window } \n "
84+ f"neighbouring_parts = { self .neighbouring_parts } \n "
85+ f"ratio = { self .ratio } \n "
86+ )
87+
88+ def get_surrounding_window (self ):
89+ self .window = []
90+ for y in [self .y - 1 , self .y , self .y + 1 ]:
91+ for x in [self .x - 1 , self .x , self .x + 1 ]:
92+ self .window .append ((y , x ))
93+
94+ def check_window_for_parts (self , parts_collection ):
95+ neighbouring_parts = []
96+ for part in parts_collection :
97+ coords_to_check = part .full_coordinates
98+ if bool (set (self .window ).intersection (coords_to_check )):
99+ neighbouring_parts .append (part .part_number )
100+
101+ self .neighbouring_parts = neighbouring_parts
102+
103+ def check_if_gear (self ):
104+ if not self .is_potential_gear :
105+ raise ValueError ("Not a potential gear!" )
106+
107+ if len (self .neighbouring_parts ) == 2 :
108+ part_numbers_as_int = [int (p ) for p in self .neighbouring_parts ]
109+ self .is_gear = True
110+ self .set_ratio (* part_numbers_as_int )
111+
112+ def set_ratio (self , n1 , n2 ):
113+ if not self .is_gear :
114+ raise ValueError ("Ratio can only be set for gears!" )
115+
116+ self .ratio = n1 * n2
117+
118+
4119def check_char_is_number (char ) -> bool :
5120 return bool (re .search (r"\d" , char ))
6121
@@ -9,52 +124,73 @@ def check_char_is_symbol(char) -> bool:
9124 return bool (re .search (r"[^.\d]" , char ))
10125
11126
12- def get_symbol_positions (input ) -> set :
13- symbol_positions = []
127+ def get_symbols (input ) -> set :
128+ symbols_list = []
14129 for line_number , line in enumerate (input ):
15130 for x_pos , char in enumerate (line ):
16131 if check_char_is_symbol (char ):
17- symbol_positions .append ((line_number , x_pos ))
18- return set (symbol_positions )
19-
20-
21- def get_window (match_start , match_end , line_number ) -> set :
22- window = []
23- for y_pos in [line_number - 1 , line_number , line_number + 1 ]:
24- for x_pos in range (match_start - 1 , match_end + 1 ):
25- window .append ((y_pos , x_pos ))
26- return set (window )
132+ sym = Symbol (char = char , y = line_number , x = x_pos )
133+ symbols_list .append (sym )
134+ return symbols_list
27135
28136
29137def check_window_for_symbol (window , symbol_positions ) -> bool :
30138 return bool (window .intersection (symbol_positions ))
31139
32140
33- def identify_part_numbers (input , symbol_positions ) -> list :
34- part_numbers = []
141+ def identify_part_numbers (input , symbols_collection ) -> list :
142+ parts_list = []
143+
144+ symbol_positions = [s .coordinates for s in symbols_collection ]
35145
36146 for line_number , line in enumerate (input ):
37147 matched_numbers = re .finditer ("(\\ d+)" , line )
38- line_sum_of_part_numbers = []
39-
40148 for match in matched_numbers :
41149 match_start = match .start ()
42150 match_end = match .end ()
43151 part_number = match .group ()
44- window = get_window (match_start , match_end , line_number )
45- if check_window_for_symbol (window , symbol_positions ):
46- part_numbers .append (part_number )
47- line_sum_of_part_numbers .append (part_number )
152+ part = Part (
153+ part_number = part_number ,
154+ )
155+ part .set_coordinates (line_number , match_start , match_end )
156+ part .get_window_around_part ()
157+ if part .check_window_for_symbol (symbol_positions ):
158+ parts_list .append (part )
159+
160+ for p in parts_list :
161+ print (p )
162+ return parts_list
163+
164+
165+ def identify_gears (parts_collection , symbol_collection ):
166+ potential_gears_to_check = [
167+ sym for sym in symbol_collection if sym .is_potential_gear
168+ ]
169+ for sym in potential_gears_to_check :
170+ sym .get_surrounding_window ()
171+ sym .check_window_for_parts (parts_collection )
172+ sym .check_if_gear ()
173+
174+ gear_collection = [sym for sym in symbol_collection if sym .is_gear ]
175+
176+ for sym in [sym for sym in symbol_collection if sym .is_potential_gear ]:
177+ print (sym )
178+
179+ return gear_collection
180+
48181
49- return part_numbers
182+ def calculate_sum_of_part_numbers (input_list ) -> int :
183+ return sum ([part .part_number_as_integer () for part in input_list ])
50184
51185
52- def calculate_sum_of_part_numbers ( input_list ) :
53- return sum (list ( map ( int , input_list )) )
186+ def calculate_sum_of_gear_ratios ( gear_collection ) -> int :
187+ return sum ([ sym . ratio for sym in gear_collection ] )
54188
55189
56- def solve_day_3 (input ):
57- symbol_positions = get_symbol_positions (input )
58- list_of_part_numbers = identify_part_numbers (input , symbol_positions )
59- sum_of_part_numbers = calculate_sum_of_part_numbers (list_of_part_numbers )
60- return sum_of_part_numbers
190+ def solve_day_3 (input ) -> int :
191+ symbol_collection = get_symbols (input )
192+ parts_collection = identify_part_numbers (input , symbol_collection )
193+ sum_of_part_numbers = calculate_sum_of_part_numbers (parts_collection )
194+ gear_collection = identify_gears (parts_collection , symbol_collection )
195+ sum_of_gear_ratios = calculate_sum_of_gear_ratios (gear_collection )
196+ return (sum_of_part_numbers , sum_of_gear_ratios )
0 commit comments